# Murph agent debug calls
Source: https://docs.apostra.com/v2/api/murph/agent-debug-calls
Inspect recent third-party sales-agent calls, including provenance for sandbox tests and live demand
Murph agent debug calls show recent AdCP activity for storefront inventory
sources backed by third-party sales agents. Use this endpoint when you need to
understand what Apostra sent to a source, what the source returned, and
whether the call came from a sandbox test run or live buyer demand.
These rows describe observed AdCP calls. For external sales-agent sources, do
not treat an empty or failed component cache as the same evidence as a failed
live passthrough call. Cache diagnostics are about whether Apostra can reuse
raw products/components for merchandising; debug-call rows are about calls that
actually went to the source.
The response also includes `transportRequests`, a bounded and redacted 30-day
ledger of outbound transport evidence. `calls` remains the logical ADCP activity
summary, so the two counts need not match. For media-buy mutations, `groupId`
identifies the parent application operation and `debugId` identifies a child SDK
session. `physicalRequestCount` counts outbound protocol attempts;
`logicalOperationCount` counts accepted operation envelopes.
Calls to the sales agent Apostra runs for an ad server source are eligible for
transport evidence; those rows have a `null` `destinationOrigin` because the
request goes to an Apostra-internal service. Transport evidence can be
incomplete: under heavy storage load Apostra drops rows rather than slow down
live requests, so a missing row does not prove that no request was sent.
An operation stage of `mutation_not_dispatched` means Apostra stopped the
operation locally. It does not mean the remote Agent rejected the request, and
it does not prove the remote state is unchanged. A later authoritative read is
required to establish that state.
This endpoint is available only when Murph is enabled for the caller's
account.
Calls are an account-wide feed unless you pass `sourceId`. Always pass the
exact Inventory Source ID before treating results as evidence for one Source;
an unfiltered response is not Source-scoped.
## Endpoint
`GET /api/v2/murph/agent-debug-calls`
## Request
```bash curl theme={null}
curl "https://api.apostra.com/api/v2/murph/agent-debug-calls?sourceId=retail-adcp&take=25" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
## Parameters
| Field | Type | Required | Notes |
| - | - | - | - |
| `storefrontId` | integer | No | Limit calls to one storefront. |
| `sourceId` | string | No | Limit calls to one storefront inventory source. |
| `taskId` | string | No | Limit calls to one AdCP task ID. |
| `debugId` | string | No | Limit calls to one Apostra debug ID (`x-scope3-debug-id`). |
| `dispatchGroupId` | string | No | Limit transport evidence to one application operation. This does not widen the customer, Source, or Storefront scope. |
| `mediaBuyId` | integer | No | Limit transport evidence to one local media-buy ID. |
| `mediaBuyIdText` | string | No | Lossless decimal media-buy ID for values above JavaScript's safe integer range. |
| `correlationId` | string | No | Match a call by operation ID, task ID, context ID, idempotency key, traceparent, request ID, or debug ID. |
| `status` | string | No | Limit calls to one task status, such as `COMPLETED`, `FAILED`, or `INPUT_REQUIRED`. |
| `windowHours` | integer | No | Trailing lookback in hours, `1`–`720` (30 days). Values above `720` return `400`. Defaults to `168` (7 days) when omitted and no `from` is given. Point lookups by task, debug, dispatch group, media buy, or correlation ID are unbounded unless you pass `windowHours` or `from`. The applied window is echoed as `window`. |
| `from` | string | No | ISO-8601 UTC start of a precise time range. When combined with `windowHours`, the effective start is whichever is later. |
| `to` | string | No | ISO-8601 UTC end of a precise time range. Defaults to now. |
| `take` | integer | No | Maximum number of rows to return (default `25`, max `100`). |
## Response
```json theme={null}
{
"calls": [
{
"id": 98765,
"storefrontId": 42,
"sourceId": "retail-adcp",
"sourceName": "Retail AdCP Agent",
"agentId": "agent_retail",
"agentName": "Retail AdCP Agent",
"operationId": "op_456",
"contextId": "ctx_789",
"idempotencyKey": "murph-test-abc",
"debugId": null,
"traceparent": null,
"requestId": null,
"taskId": "task_123",
"taskStatus": "FAILED",
"rawTaskStatus": "create_media_buy rejected",
"tool": "create_media_buy",
"activityType": "RESPONSE",
"rawActivityType": "response",
"payload": {
"error": "Billing field is required."
},
"userEmail": "s***@example.com",
"initiatorEmail": "s***@example.com",
"origin": {
"kind": "sandbox_test",
"testRunUid": "550e8400-e29b-41d4-a716-446655440001",
"testRunStatus": "failed",
"testRunToolName": "create_media_buy",
"testRunCreatedAt": "2026-06-18T12:45:00Z"
},
"timestamp": "2026-06-18T12:45:30Z"
}
],
"window": {
"hours": 168,
"requestedHours": null,
"startedAt": "2026-06-11T12:45:30Z",
"endedAt": "2026-06-18T12:45:30Z"
},
"transportRequests": {
"unavailable": false,
"totalCount": 2,
"sourceAttributedCount": 1,
"sharedAgentCount": 1,
"displayedCount": 1,
"hasMore": true,
"retentionDays": 30,
"requests": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"observedAt": "2026-06-18T12:45:30Z",
"completedAt": "2026-06-18T12:45:31Z",
"groupId": "dbg_550e8400-e29b-41d4-a716-446655440000",
"attributionScope": "source",
"origin": null,
"destinationOrigin": null,
"dispatchState": "remote_responded",
"provenance": "remote_agent",
"requestKind": "adcp_tool",
"httpMethod": "POST",
"adcpTool": "get_products",
"protocolMethod": "tools/call",
"outcome": "succeeded",
"httpStatus": 200,
"latencyMs": 120,
"errorClass": null,
"failureDomain": "none",
"instrumentationGap": false,
"requestHeaders": {},
"requestBody": null,
"requestBodyTruncated": false,
"responseHeaders": {},
"responseBody": null,
"responseBodyTruncated": false,
"statusText": null,
"replayOfTransportRequestId": null,
"replayEligible": false,
"accountId": null,
"mediaBuyId": null
}
]
}
}
```
## Transport attempts
`transportRequests` is optional while API and UI deployments roll out
independently. Current API responses include it, but clients must continue to
work when an older response does not. It is additive to `calls`; it does not
change the logical call history.
When `transportRequests.unavailable` is `true`, the transport-ledger lookup
was unavailable. The `calls` array is still returned, and the transport counts
and `requests` array are empty. Retry the request later if you need transport
attempt evidence.
| Field | Type | Notes |
| - | - | - |
| `transportRequests` | object \| omitted | Bounded physical transport-attempt evidence. Omitted only for backward compatibility with an older response. |
| `transportRequests.unavailable` | boolean | `true` means transport evidence could not be read. Logical calls remain available. |
| `transportRequests.totalCount` | integer | All matching transport attempts retained for the requested time range, before the response limit is applied. |
| `transportRequests.sourceAttributedCount` | integer | Matching attempts tied directly to an inventory source. |
| `transportRequests.sharedAgentCount` | integer | Matching account-wide attempts tied to a shared Agent source rather than a single inventory source. |
| `transportRequests.displayedCount` | integer | Number of attempt rows returned in `transportRequests.requests`. |
| `transportRequests.hasMore` | boolean | `true` when additional matching attempts exist beyond the returned rows. |
| `transportRequests.retentionDays` | integer | Days that transport-attempt evidence is retained. |
| `transportRequests.requests` | array | Bounded attempt records with timing, attribution, dispatch, protocol, outcome, and replay metadata. |
| `transportRequests.requests[].attributionScope` | `"source"` \| `"agent_global"` | `source` is directly attributed to an inventory source. `agent_global` is shared-Agent/account-wide evidence. In an unfiltered account-wide feed it can relate to more than one source; pass `sourceId` before treating it as evidence for one source. |
| `transportRequests.requests[].observedAt` | string | When Apostra observed the transport attempt. |
| `transportRequests.requests[].outcome` | string | Recorded transport outcome: `started`, `succeeded`, `http_error`, or `transport_error`. |
| `transportRequests.requests[].failureDomain` | string | Recorded failure category, when any. |
The authorised REST response and Diagnostics Calls UI can include captured
transport detail, such as request and response headers or bodies. Treat those
fields as diagnostic records for authorised operators. Murph's tool response
uses only structural transport metadata and does not send those captured values
into model context.
| Field | Type | Notes |
| - | - | - |
| `window.hours` | number \| null | Trailing hours actually covered (`endedAt` − `startedAt`); may be fractional for a precise sub-hour `from`/`to` range. `null` only for an unbounded point lookup (a `taskId`/`debugId`/`correlationId` lookup with no `windowHours`/`from`). |
| `window.requestedHours` | integer \| null | The `windowHours` you asked for, or `null` if none was supplied. Compare against `window.hours` to see whether the lookback was narrowed. |
| `window.startedAt` | string \| null | Inclusive lower bound applied to the lookup. `null` means unbounded start. |
| `window.endedAt` | string | Inclusive upper bound applied to the lookup (your `to`, or now). |
| `timestamp` | string | When Apostra observed the request, response, webhook, or status activity. |
| `userEmail` | string \| null | Redacted email on the activity row when available. |
| `initiatorEmail` | string \| null | Redacted email for the user who initiated the call. For sandbox tests, this is the Murph test-run user when the call can be matched to a test run. |
| `origin.kind` | `"sandbox_test"` \| `"live_demand"` | `sandbox_test` means the call was linked to a Murph sandbox test run or uses Murph's sandbox test idempotency marker. `live_demand` means no sandbox test provenance was found, so treat it as non-test demand traffic. |
| `origin.testRunUid` | string \| null | Test-run UID when the call can be matched to a Murph sandbox test run. |
| `origin.testRunStatus` | `"passed"` \| `"partial"` \| `"failed"` \| `"planned"` \| null | Status of the matched sandbox test run. |
| `origin.testRunToolName` | string \| null | Test-run tool name when available. |
| `origin.testRunCreatedAt` | string \| null | Creation timestamp for the matched sandbox test run. |
## Provenance
Use `origin.kind` before interpreting a failure:
* `sandbox_test` calls were created by a technical validation flow. A failed
sandbox call usually means the source or adapter needs setup, protocol, or
credential work before launch.
* `live_demand` calls were not matched to Murph sandbox test metadata. Treat
them as actual demand or other non-test traffic unless you have separate
evidence that the caller was testing manually.
When `origin.kind` is `sandbox_test`, `initiatorEmail` identifies the redacted
test-run user when that user is available. When the test-run join is not
available, Murph falls back to the redacted activity-row user email.
## Errors
* `400 VALIDATION_ERROR` — invalid query parameter.
* `401 UNAUTHORIZED` — missing or invalid bearer token.
* `403 FORBIDDEN` — Murph is not enabled for the caller's account.
See [Errors](/v2/reference/errors) for the full error contract.
# Murph chat rendering artifacts
Source: https://docs.apostra.com/v2/api/murph/chat-response
REST-only rendering artifacts and the standard MCP App result returned by ask_murph
A `POST /api/v2/murph/chat` response always carries the assistant's text
`answer`. On turns where Murph runs a tool that has a canonical interactive
surface, the response **also** carries a structured rendering artifact for the
in-product UI and other direct REST clients.
These named REST fields are not an MCP Apps protocol. External hosts such as
Claude or ChatGPT do not receive the `/murph/chat` response when they call the
`ask_murph` MCP tool; they receive the standard MCP tool result described
below.
Each artifact field is optional and nullable. A client that does not render
MCP Apps can ignore every artifact and show the text `answer` alone — the
answer is always self-contained.
## How a REST client renders an artifact
Every artifact names the MCP App that renders it and the data to hand it:
1. Load the MCP App at the artifact's `resourceUri`.
2. Pass the artifact's `data` object to the app as the tool result's
`structuredContent`. The app routes on `data.endpoint` to the named `view`.
The host decides where to place the surface — the in-product Murph UI renders
the discovery widget **above** the text answer, for example.
## `discoveryMcpui`
Emitted on a successful `discover_products` tool call. Renders the product
discovery surface — a browsable, selectable list of the discovered products
grouped by storefront, with per-product brief-relevance reasoning.
```json theme={null}
{
"discoveryMcpui": {
"kind": "discover-products",
"resourceUri": "ui://agentic-api/api-call/mcp-app.html",
"view": "discover-products",
"canonicalSurface": true,
"discoveryId": "disc_7f3a…",
"data": {
"success": true,
"status": 200,
"endpoint": "/api/v2/buyer/discovery/discover-products",
"method": "POST",
"data": {
"discoveryId": "disc_7f3a…",
"productGroups": [],
"proposals": []
}
}
}
}
```
| Field | Type | Notes |
| - | - | - |
| `kind` | `"discover-products"` | Identifies the artifact. |
| `resourceUri` | `"ui://agentic-api/api-call/mcp-app.html"` | The MCP App that renders it. |
| `view` | `"discover-products"` | The view the app routes to. |
| `canonicalSurface` | `true` | The artifact is the canonical surface for this result; do not rebuild it from the text answer. |
| `discoveryId` | string | The discovery session id, when present. Lets the widget re-fetch products (`GET /api/v2/buyer/discovery/{discoveryId}/products`) so it can self-hydrate even if `data` was truncated for size. |
| `data` | object | The `discover_products` response envelope (`success`, `status`, `endpoint`, `method`, `data`). Hand this to the app as `structuredContent`. |
When this artifact is present, Murph's text `answer` is a concise orientation
(totals, price range, any recommended plans) plus next steps — it deliberately
does **not** re-list every product, because the widget already shows the full,
selectable set.
## Other rendering artifacts
The chat response uses the same pattern for other canonical surfaces:
| Field | Emitted on | `resourceUri` | `view` |
| - | - | - | - |
| `creativeMcpui` | a successful creative-session call | `ui://agentic-api/api-call/mcp-app.html` | `creative-iteration` |
| `inventoryMcpui` | a seller inventory-selector / capabilities call | `ui://agentic-api/inventory-selector/mcp-app.html` | `inventory-selector` |
| `mcpAppWidget` | an `open_` directive (e.g. a seller setup or credential form) | the named widget's `ui://…` resource | — |
Each carries `kind`, `resourceUri`, `canonicalSurface`, and a `data` payload to
hand the app as `structuredContent`; `creativeMcpui` additionally carries a
`refresh` directive describing the tool call that reloads the surface, and
`inventoryMcpui` carries the `esaId` and the initial selector filters.
## `ask_murph` MCP tool result (conformant MCP Apps)
The artifacts above ride the **REST** `/api/v2/murph/chat` response as named
fields — the in-product Murph UI reads them directly. An external MCP Apps host
(Claude, ChatGPT) that calls the **`ask_murph` MCP tool** does not receive that
REST response directly; it receives a standard tool result. Some result kinds
retain named compatibility data inside `structuredContent`, but rendering is
selected by standard result metadata:
* It selects **one primary widget per turn** — this result contract carries a
single render payload. Precedence, highest-intent first:
`mcpAppWidget` → `creativeIntentMcpui` → `inventoryMcpui` → `creativeMcpui` →
`discoveryMcpui` → `sellerAnalytics` → `murphEscalations`.
* The result embeds that widget's `ui://` resource in `content` and sets a
per-call `_meta` (`ui.resourceUri` and the legacy `ui/resourceUri`) so the host
loads the right MCP App for this result.
* For api-call-style widgets (discovery, inventory, creative, creative-intent),
the app reads the flat api-call envelope (`success`, `endpoint`, `params`,
`data`), so that envelope is spread onto the **top level** of
`structuredContent` where `ontoolresult` reads it. The now-redundant named
field is dropped so the payload isn't carried twice.
* Launch widgets (`mcpAppWidget`) and named-field panels (`sellerAnalytics`,
`murphEscalations`) carry no api-call envelope; their data already sits under
their named key in `structuredContent`, which the host hands the app.
A turn that produced several widgets still renders **one** in a generic host;
the in-product drawer can show all of them via the REST fields above.
## Sales Agent validation handoff
The Agent page can pass a server-recognized validation context to `ask_murph`.
It supplies identifiers only: the Agent, its returned skill version, an
available profile, and, when present, a Source. It never supplies credentials
or expands the caller's authority.
A customer account runs the `brief-only` profile. The `transaction` profile is
not offered to customer accounts, and certification runs automatically on live
traffic. A request for it from a customer account returns a `VALIDATION_ERROR`
with `details.reason: "transaction_test_internal_only"`. The service starts
nothing and never runs a different test instead.
A completed result identifies its implementation revision and cleanup outcome.
It is validation evidence, not a certificate or authorization for a client
connection.
## Sales Agent live test buy request
A directly authenticated administrator of the Agent owner's organization can
ask Murph to file a request for our team to review the Agent. The request
includes the Agent, the seller's Sources for that Agent, and any declared use
cases. See [Sales Agent requirements and best
practices](/v2/storefront/inventory-sources/sales-agent-requirements) for the
evidence our team reviews.
Murph returns `salesAgentLiveTestBuyRequest` when it files that intent:
```json theme={null}
{
"salesAgentLiveTestBuyRequest": {
"askId": "2d6d3a85-0d82-4b8e-a1b2-9af79e998602",
"agentId": "190742ce-02de-4b4c-a60a-d3326d7c411e",
"inventorySourceIds": ["seller-production-source"],
"declaredUseCases": ["Display"],
"status": "FILED_FOR_LIVE_TEST_BUY"
}
}
```
`askId` identifies the request filed for the team, or is `null` when no ask ID
was returned. `agentId` identifies the Agent. `inventorySourceIds` lists the
Sources the request named, or, if it named none, every Source the seller has
for that Agent, sorted by public ID. It is empty only when the seller has no
Source for the Agent.
`declaredUseCases` contains any seller-supplied use cases. The
`FILED_FOR_LIVE_TEST_BUY` status means the request was filed; it does not mean
a test ran, an assertion passed, or the Agent is certified.
`salesAgentLiveTestBuyRequest` is a new envelope. The existing
`salesAgentValidation` and `salesAgentCertificationRequest` request and
response shapes retain their existing semantics. The latter continues to use
the protected-evaluation workflow and returns its
`QUEUED_FOR_PROTECTED_EVALUATION` receipt shape.
## Conversational MCP profile
The existing Murph MCP endpoint also exposes a portable conversation profile.
MCP clients on buyer and seller accounts use the same two tools; existing
`ask_murph` calls keep their current request and response shape.
### `chat`
Call `chat` to begin a conversation or add a turn. The request has one message
and may include up to five Base64 attachments, each no larger than 1 MiB after
decoding.
```json theme={null}
{
"conversationId": "optional-existing-conversation-id",
"message": {
"text": "Show the campaign status",
"attachments": [
{
"filename": "brief.txt",
"mimeType": "text/plain",
"base64": "..."
}
]
}
}
```
A completed response is a normal MCP `CallToolResult`. Read the portable turn
from `structuredContent.conversation`:
```json theme={null}
{
"content": [{ "type": "text", "text": "Campaign status is ready." }],
"structuredContent": {
"conversation": {
"conversationId": "conversation-id",
"turnId": "turn-id",
"status": "completed",
"messages": []
}
}
}
```
`status` is `completed`, `input_required`, or `failed`. Messages contain only
display-safe user and assistant text; tool traces, internal context, and
credentials are never returned. A protocol failure instead sets `isError` and
may return only text or an error object in `structuredContent`.
When a turn declares an app widget, it appears as `structuredContent.widget`.
The result selects the widget through the standard `_meta.ui.resourceUri`
directive. Hosts must use that shared MCP Apps contract; they must not infer a
widget or route from Murph's text.
#### Progress on a long turn
A `chat` turn runs Murph's full tool loop, so an involved question can take
minutes to answer. Set the MCP protocol's `_meta.progressToken` on the
`tools/call` request and the server sends `notifications/progress` messages
while the turn works, so a host can show the caller that a long turn is
running rather than hung.
Each notification carries a short `message` describing the current phase —
"Reading the docs…", "Checking your account…", "Writing it up…" — written in
the language the conversation is already being answered in. Internal tool
names are never sent; the phrases group tools by intent. The first
notification goes out before the turn starts, so a turn that calls no tools
still reports that it began. `progress` increments on each distinct phase and
no `total` is sent, because the number of phases is not known in advance —
render it as indeterminate progress rather than a percentage.
Progress is advisory. The tool result stays the authoritative answer, a client
may ignore the notifications, and a client that sends no progress token gets
exactly the synchronous behaviour it got before.
The portable profile also permits the MCP Tasks extension for a turn that a
server does not complete synchronously. Such a result is the native
`CreateTaskResult` task envelope (`taskId`, `status`, `createdAt`,
`lastUpdatedAt`, `ttl`, and optional `pollInterval`), which a client polls or
cancels using MCP Tasks. Murph's current orchestration completes this profile
call synchronously.
### `get_conversation`
Call `get_conversation` with a conversation ID to read display-safe turns. Set
`after` to the opaque cursor from `nextAfter` to request the following page.
```json theme={null}
{
"conversationId": "conversation-id",
"after": "optional-turn-cursor"
}
```
The structured result contains `conversationId`, `turns`, and `nextAfter` when
another page is available. It does not create an agent-initiated message or
subscribe the caller to notifications.
## Token usage
Every `POST /api/v2/murph/chat` response carries a `usage` object reporting the
turn's token consumption, cumulative across the tool-loop iterations of the turn.
```json theme={null}
{
"usage": {
"inputTokens": 1840,
"outputTokens": 320,
"cacheReadInputTokens": 11200,
"cacheCreationInputTokens": 640,
"latencyMs": 4200,
"model": "claude-sonnet-5"
}
}
```
| Field | Type | Notes |
| - | - | - |
| `inputTokens` | integer | Uncached input tokens for the turn. Does **not** include tokens served from or written to the prompt cache. |
| `outputTokens` | integer | Tokens generated in the response. |
| `cacheReadInputTokens` | integer | Input tokens served from the prompt cache. |
| `cacheCreationInputTokens` | integer | Input tokens written to the prompt cache on this turn. |
| `latencyMs` | integer | Wall-clock duration of the turn, in milliseconds. |
| `model` | string | The model that produced the answer. |
The model reports cache reads and creations **separately** from `inputTokens`
— they are not included in it. To get the true total input volume for a turn,
sum `inputTokens + cacheReadInputTokens + cacheCreationInputTokens`. Murph
caches its system prompt and tool surface, so on a warm conversation
`cacheReadInputTokens` is typically the largest of the three.
## Related
Set a Murph conversation's account or advertiser scope.
How Murph works as the in-product assistant.
# Conversation scope
Source: https://docs.apostra.com/v2/api/murph/conversation-scope
Set a Murph conversation's account or advertiser scope
Every Murph conversation has a scope: either your **account** or
a **single advertiser**. The scope is set when the conversation is created and
never changes, so a conversation's context stays stable across turns.
Account-scoped conversations are the place for cross-advertiser work;
advertiser-scoped conversations keep Murph focused on one advertiser.
In Apostra web UI, selecting an advertiser and opening Murph from that
advertiser context starts an advertiser-scoped conversation automatically.
Opening Murph from the organization context starts an account-scoped
conversation. This UI selection does not change direct Buyer API or MCP
campaign calls: those calls continue to use the advertiser or account scope in
their own request and authentication context.
These endpoints are available only when Murph is enabled for the caller's
account.
## Set a conversation scope
`POST /api/v2/murph/chat`
The first message of a conversation accepts an optional scope. Omit it —
or send `scopeType: "customer"` — for an account-scoped conversation. Send
`scopeType: "advertiser"` with the advertiser's id in `scopeId` to scope the
conversation to one advertiser.
```bash curl theme={null}
curl -X POST https://api.apostra.com/api/v2/murph/chat \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "How are my campaigns pacing?",
"scopeType": "advertiser",
"scopeId": "522"
}'
```
### Request fields
| Field | Type | Required | Notes |
| - | - | - | - |
| `scopeType` | `"customer"` \| `"advertiser"` | No | Scope for the new conversation. Defaults to `customer` when omitted. |
| `scopeId` | string | No | The advertiser id when `scopeType` is `advertiser`. Max 120 characters. Not used for `customer` scope. |
The scope is honored only on the **first** message of a conversation — it is
set once at creation and is **immutable**. Sending `scopeType`/`scopeId` on a
later turn of an existing conversation has no effect; the stored scope always
wins. To work in a different scope, start a new conversation.
## Read the scope
`GET /api/v2/murph/conversations`
Every conversation summary carries its scope, so you can group or filter
conversations by the account or advertiser they belong to. The
single-conversation endpoint, `GET /api/v2/murph/conversations/{conversationUid}`,
returns the same fields.
### Response (excerpt)
```json theme={null}
{
"conversations": [
{
"conversationUid": "9b1c…",
"scopeType": "advertiser",
"scopeId": "522"
},
{
"conversationUid": "3f0a…",
"scopeType": "customer",
"scopeId": null
}
]
}
```
| Field | Type | Notes |
| - | - | - |
| `scopeType` | `"customer"` \| `"advertiser"` | The conversation scope. Always present. |
| `scopeId` | string \| null | The advertiser id when `scopeType` is `advertiser`; `null` for account scope. |
## Search conversation history
Add a literal `search` term to the conversation-list endpoint to find a prior
Murph room by title, participant, or user/assistant transcript text:
```bash curl theme={null}
curl "https://api.apostra.com/api/v2/murph/conversations?search=campaign&take=20" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Search returns only the authenticated member's own rooms and rooms that a
teammate explicitly shared within the same account. Private teammate rooms,
other accounts, archived rooms, tool output, and system context are excluded.
The term is treated literally, may contain up to 200 characters, and does not
change the endpoint's existing pagination or authorization rules.
In the Murph rail, **All** opens the portable Conversation history Page rather
than expanding the rail. The Page is limited to the account or advertiser you
are currently working in; changing advertiser requires opening history again
from that advertiser's scope.
## Scope and access
Conversation scope is **contextual**: it organizes your conversations and frames
Murph's responses around the chosen node. It does not change what a conversation
can access. Every Murph conversation is isolated by the `customer_id` of the
authenticated caller, and that account remains the access boundary regardless of
the conversation's scope — choosing an advertiser narrows the conversation's
focus, not its permissions.
## Internal TARS project and initiative scope
Authenticated TARS PM-seat surfaces can also bind a conversation to one Linear
project or initiative. This internal scope is selected by the trusted host and
is not supplied through the public `scopeType` or `scopeId` fields above. TARS
uses it to read the canonical investment recommendation and to ensure a staged
PM investment decision can be confirmed only for the same accountable human,
project or initiative, and proposal targets that were originally displayed.
The scope is preserved with a pending confirmation so an approved replay cannot
drift to a newly selected project or initiative. Admin, ordinary TARS chat, and
Slack therefore produce the same durable decision semantics even though the
server records their provenance separately. This binding narrows the PM
conversation context; it grants no Build Room nomination, admission, launch, or
funding authority.
## Errors
* `400 VALIDATION_ERROR` — `scopeType` is not `customer` or `advertiser`, or `scopeId` exceeds 120 characters.
* `401 UNAUTHORIZED` — missing or invalid bearer token.
* `403 FORBIDDEN` — Murph is not enabled for the caller's account.
See [Errors](/v2/reference/errors) for the full error contract.
## Related
How Murph works as the in-product assistant.
Set Murph's default language and read display preferences.
# Conversation settings
Source: https://docs.apostra.com/v2/api/murph/conversation-settings
Read and set the org-level default for Murph conversation sharing
Murph conversation settings are scoped to the org (account). They control the
default sharing behavior for new Murph conversations created within the account.
These endpoints require an admin or super-admin role. Non-admin callers receive
`403 FORBIDDEN`. The endpoints are available only when Murph is enabled for the
caller's account.
## Get conversation settings
`GET /api/v2/murph/conversation-settings`
Returns the org's current conversation settings.
```bash curl theme={null}
curl https://api.apostra.com/api/v2/murph/conversation-settings \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
### Response
```json theme={null}
{
"defaultSharingEnabled": false
}
```
| Field | Type | Notes |
| - | - | - |
| `defaultSharingEnabled` | boolean | When `true`, new conversations created without an explicit `sharingEnabled` value start shared with teammates in the same account. When `false` (the default), they start private. |
## Set conversation settings
`PATCH /api/v2/murph/conversation-settings`
Sets the org-level default sharing behavior for new Murph conversations.
The setting applies only to new conversations. Existing conversations keep their
current sharing state.
```bash curl theme={null}
curl -X PATCH https://api.apostra.com/api/v2/murph/conversation-settings \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"defaultSharingEnabled": true
}'
```
### Request body
| Field | Type | Required | Notes |
| - | - | - | - |
| `defaultSharingEnabled` | boolean | Yes | `true` to share new conversations with teammates by default; `false` to make them private by default. |
### Response
```json theme={null}
{
"defaultSharingEnabled": true
}
```
The response returns the full settings object now in effect.
## How this connects to creating conversations
When you call `POST /api/v2/murph/conversations`, the `sharingEnabled` field
in the request body follows this precedence:
1. **Explicit value in the request** - if `sharingEnabled` is provided, that value is used.
2. **Org default** - if `sharingEnabled` is omitted, the org-level `defaultSharingEnabled` value is used.
3. **False** - if no org default has been set, new conversations start private.
Individual users can change sharing on any conversation they own at any time using
`PATCH /api/v2/murph/conversations/{conversationUid}/sharing`.
## Errors
* `400 VALIDATION_ERROR`: invalid request body.
* `401 UNAUTHORIZED`: missing or invalid bearer token.
* `403 FORBIDDEN`: caller does not have admin or super-admin role, or Murph is not enabled for the caller's account.
See [Errors](/v2/reference/errors) for the full error contract.
## Related
Share a Murph conversation with teammates in the same account.
Read and set the authenticated user's Murph language and display preferences.
# Murph dictation
Source: https://docs.apostra.com/v2/api/murph/dictation
Turn a short voice recording into text for the Murph message box
Dictation turns one short recording into text. The web app's microphone button
uses it: the transcript goes back into the message box for the user to review
and send. Dictation does not send a message to Murph, start a turn, or store
the recording. For how dictation looks and behaves in the app, see
[Dictate a message](/v2/ui-guide#dictate-a-message).
## Transcribe a recording
`POST /api/v2/murph/dictation`
```bash curl theme={null}
curl https://api.apostra.com/api/v2/murph/dictation \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{"audio": {"base64": "", "mimeType": "audio/webm"}, "language": "en"}'
```
### Request
| Field | Type | Notes |
| - | - | - |
| `audio.base64` | string | Required. The recording, base64-encoded. At most 7 MB once decoded |
| `audio.mimeType` | enum | Required. One of `audio/webm`, `audio/mp4`, `audio/mpeg`, `audio/wav` |
| `language` | enum | Optional. The speaker's usual chat language, used as a hint. Speech in another language is still transcribed |
### Response
```json theme={null}
{
"data": {
"transcript": "Shift the Summer Sizzle budget to CTV."
},
"error": null
}
```
| Field | Type | Notes |
| - | - | - |
| `data.transcript` | string | The spoken words as plain text. Empty when the recording held no intelligible speech |
Errors use the same envelope with `data: null` and an `error` object carrying
`code` and `message`.
### How names are spelled
The recording is transcribed by Google Cloud Speech-to-Text in the United
States, in the language given by `language` (English when it is omitted). To
spell names correctly, the request includes common advertising terms, Apostra
product terms, and the names of the caller's own account's advertisers,
campaigns and storefront as spelling hints. Names from one account are never
included in another account's request. A recording with no recognizable speech
returns an empty `transcript`.
### Limits
Dictation is refused with `429` once the caller has reached their daily Murph
usage limit, the same limit chat turns use.
### Errors
| Status | Code | When |
| - | - | - |
| `400` | `VALIDATION_ERROR` | The body is malformed, the format is not one of the four accepted types, or the recording is empty or larger than 7 MB |
| `401` | — | The request is not authenticated |
| `429` | `RATE_LIMITED` | The caller's daily Murph usage limit is reached. `details.resetAt` gives the reset time when known |
| `503` | `SERVICE_UNAVAILABLE` | Transcription is unavailable or failed for this recording. Retrying later may succeed |
# Shared conversations
Source: https://docs.apostra.com/v2/api/murph/shared-conversations
Share a Murph conversation with teammates in the same account
Shared Murph conversations let teammates in the same account open the
same conversation, read the full message history, see who else is present, and
send room messages alongside assistant-directed turns.
Shared conversations are available to all customers. If the capability is
disabled for an account, these endpoints either omit shared-room fields or
return `404 NOT_FOUND` for shared-room actions.
When `sharingEnabled` is omitted from `POST /api/v2/murph/conversations`, the
new conversation inherits the org-level default set via
[conversation settings](/v2/api/murph/conversation-settings). If no org default
is configured, conversations start private.
## List conversations
`GET /api/v2/murph/conversations`
When shared conversations are enabled, the conversation list includes private
conversations owned by the caller and shared conversations in the same
account.
### Response fields
| Field | Type | Notes |
| - | - | - |
| `conversations[].sharingEnabled` | boolean | `true` when teammates in the same account can open the room link. |
| `presenceByConversationUid` | object | Map from `conversationUid` to currently active participants. Empty when shared conversations are disabled. |
```json theme={null}
{
"conversations": [
{
"conversationUid": "550e8400-e29b-41d4-a716-446655440000",
"title": "Campaign pacing",
"sharingEnabled": true
}
],
"presenceByConversationUid": {
"550e8400-e29b-41d4-a716-446655440000": [
{
"userId": 12,
"name": "Inez",
"email": "inez@example.com"
}
]
},
"totalCount": 1,
"hasMore": false
}
```
## Rename a conversation
`PATCH /api/v2/murph/conversations/{conversationUid}/title`
Overwrites the conversation title with a name you choose. The auto-generated
title is replaced immediately everywhere it appears. Only the conversation
owner can rename it.
```bash curl theme={null}
curl -X PATCH https://api.apostra.com/api/v2/murph/conversations/550e8400-e29b-41d4-a716-446655440000/title \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Demo: Add Creatives"
}'
```
### Request body
| Field | Type | Required | Notes |
| - | - | - | - |
| `title` | string | Yes | 1–255 characters. Leading and trailing whitespace is trimmed. |
### Response
```json theme={null}
{
"conversation": {
"conversationUid": "550e8400-e29b-41d4-a716-446655440000",
"title": "Demo: Add Creatives"
}
}
```
## Enable or disable sharing
`PATCH /api/v2/murph/conversations/{conversationUid}/sharing`
Sets whether a conversation is shared with teammates in the same account.
The caller must be able to read the conversation. Cross-account access
is never allowed.
```bash curl theme={null}
curl -X PATCH https://api.apostra.com/api/v2/murph/conversations/550e8400-e29b-41d4-a716-446655440000/sharing \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sharingEnabled": true
}'
```
### Request body
| Field | Type | Required | Notes |
| - | - | - | - |
| `sharingEnabled` | boolean | Yes | `true` shares the room; `false` returns it to a private conversation. |
### Response
```json theme={null}
{
"conversation": {
"conversationUid": "550e8400-e29b-41d4-a716-446655440000",
"sharingEnabled": true
}
}
```
## Stream room events
`GET /api/v2/murph/conversations/{conversationUid}/events`
Opens a Server-Sent Events stream for a shared room. The stream emits new
persisted messages and presence changes. Pass `after` to resume after a known
message sequence.
```bash curl theme={null}
curl -N "https://api.apostra.com/api/v2/murph/conversations/550e8400-e29b-41d4-a716-446655440000/events?after=12" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
### Events
| Event | Data | Notes |
| - | - | - |
| `ready` | `{ conversationUid, after }` | The stream is open. `after` is the sequence cursor the server will tail from. |
| `message.created` | `{ conversationUid, message }` | A newly persisted Murph message. `message.audience` is `assistant` or `room`; user-authored messages include author fields. |
| `presence.changed` | `{ conversationUid, participants }` | Current room participants. Participants include `userId`, `name`, and `email` when available. |
| `error` | `{ code, message, status }` | Stream-level failure. Reconnect from the latest known sequence. |
## Send a room message
`POST /api/v2/murph/conversations/{conversationUid}/messages`
Send a message to the room without asking Murph to respond by setting
`audience: "room"`. To ask Murph, use the normal chat endpoint and direct the
turn to Murph.
```bash curl theme={null}
curl -X POST https://api.apostra.com/api/v2/murph/conversations/550e8400-e29b-41d4-a716-446655440000/messages \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"audience": "room",
"content": "@inez can you check this pacing note?"
}'
```
### Request body
| Field | Type | Required | Notes |
| - | - | - | - |
| `audience` | `"room"` | Yes | Room messages are visible to participants and do not prompt Murph. |
| `content` | string | Yes | Message text. |
## Errors
* `400 VALIDATION_ERROR` — invalid UUID, query string, or request body.
* `401 UNAUTHORIZED` — missing or invalid bearer token.
* `404 NOT_FOUND` — conversation does not exist, belongs to another account,
is private to another user, or shared conversations are disabled.
See [Errors](/v2/reference/errors) for the full error contract.
## Related
Set a Murph conversation's account or advertiser scope.
Read the artifacts Murph can return from an assistant-directed turn.
# Murph user preferences
Source: https://docs.apostra.com/v2/api/murph/user-preferences
Read and set the authenticated user's Murph language and display preferences
Murph user preferences are scoped to the authenticated user. They control the
default language for new Murph conversations and expose display preferences the
UI can use when formatting Murph surfaces.
These endpoints are available only when Murph is enabled for the caller's
account.
## Get preferences
`GET /api/v2/murph/user-preferences`
Returns the caller's current Murph preferences. If the request is authenticated
with a service token instead of a user session, the endpoint returns the same
shape with all preference fields set to `null`.
```bash curl theme={null}
curl https://api.apostra.com/api/v2/murph/user-preferences \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
### Response
```json theme={null}
{
"preferredLanguage": "de",
"locale": "de-DE",
"timezone": "Europe/Berlin",
"displayCurrency": "EUR"
}
```
| Field | Type | Notes |
| - | - | - |
| `preferredLanguage` | enum \| null | User-level default language for new Murph conversations. `null` means Murph auto-detects language |
| `locale` | string \| null | BCP-47 display locale, such as `en-US`, `de-DE`, or `ja-JP` |
| `timezone` | string \| null | IANA timezone, such as `America/New_York` or `Europe/Berlin` |
| `displayCurrency` | string \| null | ISO 4217 currency code used for display formatting, such as `USD`, `EUR`, or `JPY` |
## Set default language
`PUT /api/v2/murph/user-preferences/language`
Sets the caller's user-level default Murph language. New conversations inherit
this value at creation time. Existing conversations keep their own language
override unless you update them separately.
Send `preferredLanguage: null` to clear the user default and return new
conversations to auto-detect.
```bash curl theme={null}
curl -X PUT https://api.apostra.com/api/v2/murph/user-preferences/language \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"preferredLanguage": "fr"
}'
```
### Request body
| Field | Type | Required | Notes |
| - | - | - | - |
| `preferredLanguage` | enum \| null | Yes | Supported Murph language code, or `null` to clear the default |
### Response
```json theme={null}
{
"preferredLanguage": "fr",
"locale": "fr-FR",
"timezone": null,
"displayCurrency": null
}
```
The response returns the full preference object now in effect.
## Supported languages
| Code | Language |
| - | - |
| `en` | English |
| `de` | German |
| `fr` | French |
| `es` | Spanish |
| `pt` | Portuguese |
| `it` | Italian |
| `nl` | Dutch |
| `sv` | Swedish |
| `pl` | Polish |
| `ja` | Japanese |
| `ko` | Korean |
| `zh` | Chinese, Simplified |
| `ar` | Arabic |
| `hi` | Hindi |
| `tr` | Turkish |
## Errors
* `400 VALIDATION_ERROR` — invalid request body, unsupported language code, invalid locale, invalid timezone, or invalid currency.
* `400 BAD_REQUEST` — setting a user-level default was attempted from a non-user auth context.
* `401 UNAUTHORIZED` — missing or invalid bearer token.
* `403 FORBIDDEN` — Murph is not enabled for the caller's account.
See [Errors](/v2/reference/errors) for the full error contract.
## Related
How storefront operators use Murph in the platform.
Confirmation flows for Murph actions that need approval.
# One agent. Every seller. Every channel.
Source: https://docs.apostra.com/v2/buyer-introduction
A technical introduction to building buyer agents on Apostra — for the engineers wiring the next twenty years of media buying.
> Coming from the [Apostra overview](/v2/introduction)? This is the buyer-side deep dive — what you integrate, in the order you'll integrate it.
First call in five minutes — sandbox advertiser, MCP connector, REST curl.
The single URL your agent reads at startup to learn the entire API.
OAuth into Claude or ChatGPT and drive Apostra from any agent.
## You've integrated this market before.
If you've shipped media-buying software in the last decade, you've written the same code five times. One adapter for each DSP. A separate SDK for CTV. Another for retail media. A scraper for the platform that still doesn't have an API. Every channel speaks a different language, and your integration backlog grows linearly with the market.
That's not a protocol problem. That's the absence of one.
## The shift.
Programmatic exposes a **valuation API**: bid on this impression, yes or no, in 100ms. Agentic exposes an **allocation API**: here is a brief, here are constraints, here is a budget — discover sellers, negotiate terms, execute, report.
You stop integrating vendors. You start shipping a buyer agent.
> Programmatic asks: what is this impression worth?
> Agentic asks: how should I allocate this budget?
> One is a bidder. The other is a portfolio manager. They are different programs.
## What you actually integrate.
You integrate the **Apostra**. Three surfaces, one product:
* **`skill.md`** — a single URL your agent reads at startup to learn the entire API. No SDK. No client generation. The agent reads the manual.
* **MCP** — three tools (`health`, `ask_about_capability`, `api_call`) drive the full Apostra from any MCP-speaking model. No per-domain tool sprawl.
* **REST + OpenAPI** — for CLIs, servers, and integrations that don't speak MCP. OAuth for agent connectors, API keys for everything else.
Apostra speaks **AdCP**, the open protocol for agentic media buying. That's how one integration reaches every seller — CTV, podcasts, social, retail media, OOH, anything that registers a storefront. A new channel doesn't mean a new integration on your side. The seller registers; you reach them.
AdCP moves the transaction. Apostra handles the layers the protocol can't — contracts, billing, multi-agent aggregation, governance.
## Where this leaves you.
One buyer agent. Every seller in the marketplace. One auth surface. One reporting shape. The integration cost of adding a new channel approaches zero — because you don't add it. The seller does, by registering a storefront.
That's the change. The rest of this site explains how to ship it.
Connect, create advertisers, and launch your first campaign.
Every REST endpoint with request/response schemas.
The conceptual model behind storefronts, signals, and agent-first design.
The open protocol Apostra speaks. Apache 2.0.
# Market readiness preview
Source: https://docs.apostra.com/v2/buyer/account/market-readiness
Check pilot availability and public-safe supply evidence by country and channel.
Market readiness answers separate questions before buyer setup: **is the
product/support path ready for this ISO country × channel cell, has market/legal
availability been assessed, and what public-safe supply applies?**
It does not admit an account. Buyer signup and every authenticated buyer API
remain behind the existing `alpha-opt-in` gate. A storefront appearing in the
supply read also never opens a pilot cell; opening and closing are explicit,
audited human decisions.
Buyer Setup is a separate account-level process: an account administrator
proves the buyer's stable Organization/operator identity, chooses a plan,
accepts Terms, and then connects seller and platform accounts. Billing details (payer
name, street address, and country) are required only for consolidated-billing routes;
no card or credit line is required. Market readiness never creates, confirms, or locks an
operator identity. See [Buyer setup and go-live](/v2/buyer/account/setup).
## Request readiness
Call `POST /api/v2/market-readiness` with one to 50 cells. This endpoint is
public and rate-limited. Country codes use assigned ISO 3166-1 alpha-2 values.
Send country codes in uppercase. Supported canonical channels are `audio`,
`ctv`, `display`, `dooh`, `social`, `meta`, and `google`; channel values are
lowercase. `meta` and `google` are globally governed platform overlays.
```json theme={null}
{
"cells": [
{ "countryCode": "FR", "channel": "display" },
{ "countryCode": "ID", "channel": "social" }
]
}
```
Each result includes:
* `pilotState`: the effective `open` or `closed` decision;
* `pilotScope`: `domestic`, `global_overlay`, or `none`;
* `domesticSupply` and `globalSupply`, kept separate;
* `signupGuidance`, which keeps product readiness, currently unassessed
market/legal availability, and supply readiness separate and offers scoped
next steps;
* up to 10 public names from human-reviewed listed storefronts (while
`storefrontCount` remains the full usable count); and
* `sourceHealth`, which is `healthy`, `degraded`, or `unknown`.
Only active sources from human-reviewed `LISTED` storefronts contribute to
public status, counts, names, or health. Hidden storefronts cannot be inferred
from a count, and pending or disabled sources are not treated as available
supply.
Global supply is an overlay, not a made-up country. An open global social path
can therefore apply to Indonesia while Indonesia display remains closed.
```json theme={null}
{
"data": {
"cells": [
{
"countryCode": "ID",
"channel": "social",
"pilotState": "open",
"pilotScope": "global_overlay",
"domesticSupply": {
"scope": "domestic",
"status": "none",
"storefrontCount": 0,
"namedStorefronts": [],
"sourceHealth": "unknown"
},
"globalSupply": {
"scope": "global",
"status": "available",
"storefrontCount": 1,
"namedStorefronts": [
{
"name": "Global Social Platform",
"domain": "social.example"
}
],
"sourceHealth": "healthy"
},
"signupGuidance": {
"route": "proceed",
"productReadiness": "ready",
"marketAvailability": "not_assessed",
"supplyReadiness": "available",
"options": ["continue"]
}
}
]
},
"error": null
}
```
## Unknown is not empty
If the supply query or source-health projection fails, the endpoint returns
`status: "unknown"`, `storefrontCount: null`, and
`sourceHealth: "unknown"`. It never turns a failed read into a claim that the
market has no supply.
`status: "none"` is returned only after the supply read succeeds and finds no
usable source for that scope.
A supply gap does not close signup. Its guidance includes continuing setup,
joining the exact cell's waitlist, bringing supply, and browsing other cells.
Only `route: "waitlist_product"` stops the requested product cell, and it does
not create an Account-wide waitlist.
When the signup experience records a waitlist choice, it preserves the exact
product, country, channel, desired path, notification preference, and readiness
reason shown here. Before identity proof, the record is tied to a one-way
principal reference rather than an Account. Organization linkage is added only
after a valid claim, and the interest is never used as authorization. Email
notifications also retain the accepted notice key, version, locale, rendered
text, and server-stamped consent time. Public interest remains ineligible for
email delivery until principal verification. Claimed interests can be changed
only through an authenticated Organization surface.
# Account
Source: https://docs.apostra.com/v2/buyer/account/overview
Your current account context, the accounts under your organization, membership access, and notification preferences
An **account** is the entity the authenticated user is operating as. Every buyer request runs in the context of one account, identified by a numeric `id` and carrying a `role` (`MEMBER`, `ADMIN`, or `SUPER_ADMIN`) that gates what the caller can do. A user can belong to several accounts; the account API lets you read the current context, list the accounts you can access, and switch your integration between them.
## Account model
Full user-context responses returned by account creation and switching separate
the product an account uses from its access and approval state:
| Field | Meaning |
| - | - |
| `nodeKind` | `ACCOUNT` for a product workspace or `CONTAINER` for an organization that governs accounts. |
| `accountType` | `BUYER` or `SELLER` for accounts, or `DEVELOPER` for a Developer account, which owns agents and has no Buyer or Seller tools (Developer accounts are in a closed preview). Some compatibility responses use `PARTNER` to project the organization's sales-agent offering; it is not a separate account. An organization container has no core account type. |
| `customerRole` | Compatibility field for Buyer and Seller integrations. Use `accountType` for new Buyer and Seller reads. The sales-agent offering has no `customerRole` equivalent, and a Developer account's `customerRole` is a placeholder: read `accountType` to tell it apart from a Buyer account. |
| `enabled` | Whether people and API clients can enter the account. It does not grant Buyer live-spend eligibility or Partner registration, certification, or commercial approval. |
| `buyerAccessPosture` | Deprecated compatibility projection. It is not operation authority; use Buyer Setup capability verdicts. |
| `registrationStatus` / `commercialStatus` | Independent states on an organization's sales-agent offering. Registration is `AVAILABLE` or `REGISTERED`; commercial status is `INACTIVE`, `ACTIVE`, `SUSPENDED`, or `EXITED`. A newly enabled offering starts `AVAILABLE` / `INACTIVE`, and registration or certification does not grant commercial approval. |
Each account has its own registered `customerDomain` and membership settings.
When an enterprise organization manages more than one account, organization
membership grants management access through that hierarchy but does not create
direct account membership. Separately, each user manages their own
**notification preferences** — the set of event types and channels (`email`,
`in_app`) they opt into.
## Key concepts
| Concept | Description |
| - | - |
| Current account | The account context the request authenticates as — `id`, `company`, `name`, `role`, `customerDomain` |
| Organization | The top-level entity in the hierarchy; only accounts created under an organization can be deleted |
| `customerDomain` | The account's registered domain; required before domain auto-join can be enabled |
| Operator identity | The seller-facing AdCP key for this buyer account: its operator domain and, only for a specific operating unit, a stable `operator_unit.id` |
| Membership | Per-account access settings, notably `allowDomainAutoJoin`; defaults on for top-level organizations and off for child accounts |
| Notification preferences | Per-user opt-ins of `notificationType` × `channel` |
| Role | `MEMBER`, `ADMIN`, or `SUPER_ADMIN`; admin-only operations require `ADMIN` on the target account |
## Manage child accounts in the app
Organization administrators can open **Account settings → Organization →
Accounts** from the organization or one of its child accounts. The list shows
active accounts first; use the status filter to view archived accounts. The pane
links to each active account's members and provides **Add account**, **Archive**,
and **Restore**. Administrators can see **Add account** even when creation is
unavailable; the pane explains why it is disabled. Creation follows the
organization's account-capacity rules. Archive shows any campaign or media-buy
blockers before you confirm; archived accounts retain their history and still
count toward capacity.
To archive the buyer account you are working in, open **Account settings →
Danger zone**. Organization administrators get **Open organization accounts**,
which switches to the organization and opens its account list, because an
account cannot be archived while you are using it. Other members are asked to
contact an organization administrator. Standalone buyer accounts and buyer
organizations have no Danger zone: only an organization's child accounts can
be archived.
Organizations with no separate Organization group, such as standalone accounts,
continue to use **Plan & Billing** for account usage, fees when available, and
agreement history.
## Manage accounts with a V3 agent
From an organization in the V3 MCP preview, use
`search({kind: "account"})` to list its active and archived child accounts.
Use `get({kind: "account", id: ""})` for one account. The detail
includes its `ACTIVE` or `ARCHIVED` lifecycle status and the organization's
commercial terms that the account inherits: the current Terms of Service URL,
required and accepted versions, acceptance details, current contract posture,
and up to 25 agreement-history entries. These terms belong to the organization;
they are not separate child-account terms.
Use `search({kind: "member", filter: {accountId: }})` to read an
account roster, then `get({kind: "member", id: "", accountId: })` for one membership and role. Member reads do not grant or
change access.
An organization administrator can call `save_account` for an existing direct
child. It accepts only `name` and `settings.company`. The V3 agent surface does
not choose the first owner, change members or roles, or archive and restore
accounts. Use the Organization Task below for a reviewed lifecycle request.
Member access stays in **Organization → Members**.
To inspect one managed child from a Buyer or Seller home account, a direct
organization administrator or direct Scope3 platform administrator can call
`manage_organization` with `action: "inspect_account"`, the exact
`parentCustomerId` and `customerId`. The read checks authority on that parent,
confirms the child's organization identity, and returns its lifecycle status,
archive blockers and parent workspace capacity. It does not switch accounts or
change anything. A capped organization list cannot prove that a child is absent.
To see the organization itself, use `action: "inspect_organization"` with the
exact `parentId`. It returns the organization's total, active and inactive child
account counts and its workspace capacity.
To request a standard child from the same home account, first call
`manage_organization` with `action: "review_create_account"` and the proposed
fields. The review says which capacity the account would use and whether it
can be requested, and changes nothing. Then call it with
`action: "request_create_account"`, the confirmed
`orgRef`, exact parent ID, proposed name, Buyer or Seller role, a reason and a
stable UUID. A direct organization administrator may request a standard Account
under their own organization. A direct Scope3 platform administrator may request
one under any exact organization. That administrator can use the separate
`review_demo_storefront` and `request_demo_storefront` actions for a Demo
Storefront. Standard Accounts
need contracted workspace capacity and plan coverage. Demo Storefronts are exempt
from contracted capacity. The tool returns a link. Nothing is created until a
person with the required role approves the exact request on the website. Repeat
the same call and UUID to check the outcome. No customer switch is needed.
To archive or restore one child, first call `manage_organization` with
`action: "review_account_lifecycle"`, the exact parent and child IDs, confirmed
`orgRef`, current child name, action and reason. Review the returned blockers.
Then call `manage_organization` again with
`action: "request_account_lifecycle"`, the same fields and a stable UUID. It
returns a website approval link; no lifecycle change occurs until an
eligible administrator approves it. Archive refuses active work, and restore
requires a lifecycle-archived child and a plan that still covers it. Repeat
the request with the same UUID to check its recorded outcome.
To rename one child, call `manage_organization` with
`action: "review_rename_account"`, the exact parent and child IDs, confirmed
`orgRef`, the current name from inspection, the new name and a reason. Then
call it with `action: "request_rename_account"`, the same fields and a stable
UUID. The rename waits for website approval, and it is refused if the current
name changed after the request.
To check a request later without resending it, call `manage_organization` with
`action: "check_request_outcome"`, the exact `parentId`, confirmed `orgRef`, the
kind of request (`create_account`, `demo_storefront`, `account_lifecycle` or
`rename_account`) and its original UUID. It returns the recorded state and, once
the change ran, its result.
## Create a Buyer child through MCP
From an organization in the V3 MCP preview, an agent can create a Buyer child
account under that exact organization. Nothing is created until an organization
administrator approves it on the Apostra site:
If the agent starts in a Buyer Account, call `get_status` to find the
Organization account ID, then call `switch_account` with that ID. The agent can
call `get_status` again in the Organization to see its child accounts and can
switch back to a Buyer Account afterward. An MCP host that keeps its initial
tool list also lists the Organization tools when the signed-in user can reach
the Organization. Each tool still checks the selected account and the caller's
permissions when it runs.
1. `review_buyer_child_account` with the organization's ID and the proposed
name shows the contracted account capacity, who is asking, and what will
change. It is read-only.
2. `request_buyer_child_account` with the same organization ID, the name and an
`idempotencyKey` records the request and returns `approval_required` with a
link to the approval page. Show the person the link. If the organization has
no account capacity left, the request is refused and nothing is recorded.
3. An organization administrator opens the link, signed in in their own
browser, and approves or rejects the request. It can be a different
administrator from the one who asked. An unanswered request expires after
24 hours. See [Approving agent requests](/v2/concepts/web-approvals).
4. Call `request_buyer_child_account` again with the same key and name to get
the outcome: the new account's ID once it is created, or the reason it could
not be created (for example, no account capacity left).
Creating the account sends no invitation, changes no feature flag and creates no
media buy. Organization administrators inherit access to the new account. It
does not make the account ready to buy: complete Buyer Setup, and any required
terms, plan and payment steps, separately. This works in text-only MCP hosts.
## Task reference
Confirm whole-operator or specific-unit scope before new AdCP 3.2 provisioning
`GET /accounts/current` — your current context
`GET /accounts` — accounts you can access
`PATCH /accounts/:customerId/domain` — set the registered domain
`GET /accounts/:customerId/membership` — read access settings
`PATCH /accounts/:customerId/membership` — toggle domain auto-join
`GET /notification-preferences` — your opt-ins
`PUT /notification-preferences` — replace your opt-ins
# Refer a company
Source: https://docs.apostra.com/v2/buyer/account/referrals
Introduce another company to Apostra and earn Intelligence Units after a successful referral
The customer referral program lets a signed-in Apostra customer introduce
another company to Apostra. You can submit a referral from **Refer a company** in
the account menu and follow its status on the same page.
## What to share
Submit one person at the referred company with their:
* name;
* work email;
* company name; and
* optional context about why the company may be a good fit.
Personal email addresses and referrals to your own company are not eligible.
Apostra handles outreach and review; submitting the form does not itself earn a
reward.
## When a referral is successful
A referral becomes successful after Apostra links it to a new, active
Apostra customer and confirms either:
* the referred company has started a paid plan; or
* the referred company is a buyer that has reached the agentic media-spend
threshold shown on the referral page.
A customer can qualify for one referral reward, and the referring and referred
organizations must be different.
When the referral succeeds, the offer shown on the referral page is added to
the referring organization's shared Intelligence Unit wallet. Everyone in the
organization sees the same reward; it is not a personal balance.
The page shows the current IU amount and expiry period. Referral IUs have no
cash value, do not roll over, and can be used for IU-rated Apostra activity
available to your organization. Apostra reviews eligibility and may change or
end the offer.
## Referral statuses
* **Submitted** — Apostra received the introduction.
* **Qualified** — the company appears to be a fit and review is progressing.
* **Reward issued** — the company became an active customer and the IU credit
was added to your organization's wallet.
* **Not eligible** — the referral did not meet the program rules.
# Buyer setup and go-live
Source: https://docs.apostra.com/v2/buyer/account/setup
Set up your operator once, then see what each seller or buying platform needs.
Buyer Setup separates your organization’s platform readiness from the setup for
each seller or buying platform.
If Scope3 invites you to a new buyer account, the setup email is sent to your
named address. Accept the invitation and verify that email to join. The invite
opens the account setup path; your organization still completes its domain,
plan, terms, and other readiness steps before live buying.
Buyer and Seller Setup use the same activation pattern: a neutral status and
progress header, one operator-domain editor, and a clear next action. They are
separate Pages because the work after identity is different. Buyers configure
destinations and advertiser mappings; storefronts configure inventory,
publisher authorization, settlement, and payouts.
## Platform go-live
You complete these steps once for the buyer account:
* confirm the domain of the organization operating the account;
* say whether the account represents the whole operator or a specific operating
unit;
* choose the plan for this buyer account;
* accept the current terms;
* resolve any account hold; and
* once choosing payment terms is available, choose the payment terms sellers
are asked for before buying from a seller on consolidated billing (see
[Your organization's payment terms](#your-organizations-payment-terms)).
These facts unlock real non-spend work such as connections, proposals, and
campaign setup. Your billing details are needed only when a transaction's
seller bills through Apostra on consolidated billing. Apostra does not grant
or manage a buyer credit line — sellers decide credit on their own terms, on
their own media.
Together, the domain and optional operating-unit ID form the account's AdCP
operator identity:
| Account scope | Operator identity |
| - | - |
| The whole operator | The operator domain, with no operating unit |
| A specific unit | The operator domain plus a stable `operator_unit.id` |
Use a specific unit for a buying seat, office, region, or team that needs to be
distinguishable from other accounts under the same domain. Prefer a code your
organization already uses. If you do not have one, use a short, readable ID
that will remain meaningful over time. Do not use `default`, a temporary display
name, a seller's account ID, or an Apostra database ID. The buyer account
name is mutable display metadata; the billing-company name is separate and does
not identify the operating unit.
An account administrator must confirm the scope. Before confirming, review the
other buyer accounts under the same operator domain so you do not claim the
whole operator when the account actually represents one unit. Buyer Setup lists
the confirmed identities visible in your account hierarchy for the domain you
enter. Apostra rejects an exact identity already claimed by another buyer
account. You can change the operator domain, scope, or unit ID after setup and
after connecting storefronts, until an advertiser account is created or bound
under that identity. Buyer Setup then reports the identity as locked. In AdCP
3.2, the operator domain and optional operating-unit ID are advertiser natural-key
dimensions; pre-3.2 bindings still make the seller-visible operator domain
immutable. Changing either requires an advertiser identity migration rather
than an ordinary settings update.
Apostra compares the operator domain with account membership and the
verified organization domain. The Organization must prove control before real
operations are available. AAO corporate metadata can suggest or classify the
Organization, but a public AAO record is not ownership proof. A buyer operator does not publish
`adagents.json`; that document belongs to seller authorization. Buyer setup also
does not require `brand.json`.
Buyer Setup always shows the current operator. A domain suggested from existing
account or work-email information is prefilled, but it is not silently treated
as your choice. A sole buyer account may show **The whole operator** as a
recommendation; an administrator still confirms it. When the account is a
specific unit, Buyer Setup proposes a readable ID from the account name, which
you should replace when your organization has an established code.
Existing accounts with a proved legacy domain remain able to use their existing
buying paths while scope confirmation is pending. Buyer Setup continues to mark
the operator-identity step as incomplete, and new AdCP 3.2 provisioning may
require the complete confirmed identity.
## If an API call requires Buyer Setup
On `/mcp/v3`, call `get_status` first. When
`operatorIdentity.usableForBuying` is false, an account administrator can call
`save_buyer_operator` with the real non-platform `operatorDomain` and either
`operatorScope: "whole_operator"` or `operatorScope: "specific_unit"` plus a
stable AdCP `operator_unit` (`{ "id": "east-coast" }`). When the domain is usable but `scopeStatus` is
`unclassified` and `locked` is false, reuse that domain and choose its scope.
When `locked` is true, follow the support action returned by `get_status`
instead. The tool confirms commercial identity only; it does not grant user
access or change the login organization.
Before that identity is claimed, `/mcp/v3` can still create, update, or archive
a test-only advertiser with `save_advertiser` and `sandbox: true`. This exception
applies only to the operator-identity check: plan selection, current Terms, and
other readiness blockers still apply. Live advertisers continue to require the
complete buyer operator identity.
Buyer discovery and other new buying operations return HTTP `409` with error
code `BUYER_SETUP_REQUIRED` when the account has no confirmed, non-platform
operator identity. For a missing identity, the exact structured details are
`reason: "missing_operator_identity"`, `readinessCheck:
"operator_identity"`, `setupStep: 1`, and `setupPage: "buyer_setup"`. This is
a correctable account-readiness task, not a seller outage: open **Buyer
Setup**, confirm the operator domain and account scope, then retry the request.
```json theme={null}
{
"data": null,
"error": {
"code": "BUYER_SETUP_REQUIRED",
"message": "Complete Buyer Setup step 1 by confirming your buyer operator identity before continuing.",
"details": {
"reason": "missing_operator_identity",
"readinessCheck": "operator_identity",
"setupStep": 1,
"setupPage": "buyer_setup"
}
}
}
```
Apostra never substitutes `interchange.io` for a missing buyer operator.
That would combine unrelated buyers under one seller-side natural-key account.
If a legacy profile explicitly contains that platform domain, the request uses
the same `BUYER_SETUP_REQUIRED` contract with `details.reason:
"platform_operator_not_allowed"`. Replace it with your organization’s operator
domain in **Buyer Setup**.
Changing or confirming the operator affects new discovery and provisioning,
but does not rewrite existing transactions. A historical media buy continues
to use the exact seller account persisted when the buy was created, including
for status, creative re-sync, update, cancellation, and delivery operations.
The same identity gates account linking. A production entry on
`POST /api/v2/buyer/storefront-accounts/sync` (or the
`sync_buyer_storefront_accounts` tool) must name your confirmed operator
domain as `operator`; a different domain returns HTTP `403` with
`ACCESS_DENIED` and `details.reason: "operator_identity_mismatch"`, and a
missing identity returns the `BUYER_SETUP_REQUIRED` contract above. A
sandbox entry (`sandbox: true`) is fenced off from production spend and may
name any operator.
An account grant says whether the seller will transact with you. It does not
say whether Apostra uses that seller for a given advertiser. A
`GET /api/v2/buyer/storefront-accounts` request (or the
`list_buyer_storefront_accounts` tool) that names an advertiser with
`advertiserId`, or selects one with the `x-scope3-seat-id` header, also returns
`ext.interchange.advertiser_activation`: `seller_on` (whether product discovery
and new media buys use this seller for that advertiser) and `reason` (the
control that decided it). A named advertiser you cannot read is refused with
`403`, and `503` means access could not be checked right now. A seller can have an `active` grant and still be off for the
advertiser; turn it on with
[Update advertiser Seller preference](/v2/buyer/storefronts/tasks/update-advertiser-activation-preference).
The field is omitted when the request names and selects no advertiser, when
the advertiser belongs to another account (a sibling account in your
organization, or an organization that delegated it), or when Apostra cannot
read the advertiser or the decision; the grant list still returns.
### Billing modes and the business entity payload
Each `sync_accounts` entry names a `billing` mode: `agent` (Interchange
invoices on your behalf; no additional payload needed), `operator`, or
`advertiser` (the storefront invoices your operator or advertiser directly).
`operator` and `advertiser` entries require a `billing_entity` — the AdCP
`BusinessEntity` payload with your legal name and, as applicable, tax/VAT/
registration ids, a postal address, billing contacts, and bank details:
```json theme={null}
{
"billing": "operator",
"billing_entity": {
"legal_name": "Acme Media Ltd",
"vat_id": "DE123456789",
"address": {
"street": "Friedrichstrasse 100",
"city": "Berlin",
"postal_code": "10117",
"country": "DE"
},
"contacts": [
{ "role": "billing", "name": "AP Department", "email": "ap@acme.example" }
]
}
}
```
`billing_entity.bank` is write-only: send it to provide payment coordinates,
but it is never echoed back on a `sync_accounts` or `list_accounts` response —
the seller stores it and confirms receipt without returning it.
A storefront only accepts the billing modes it has published support for. An
entry naming an unsupported mode returns HTTP `400` with
`BILLING_NOT_SUPPORTED` and `details.supported_billing` listing the modes the
storefront actually accepts:
```json theme={null}
{
"data": null,
"error": {
"code": "BILLING_NOT_SUPPORTED",
"message": "billing=operator is not supported by this storefront. Supported: agent.",
"details": {
"reason": "billing_not_supported",
"supported_billing": ["agent"],
"requested_billing": "operator"
}
}
}
```
### Payment terms
Payment terms can be requested per seller account, and separately for your
whole organization. They are two different settings with different values:
* **Per seller account (`sync_accounts`).** Each entry in a
`sync_buyer_storefront_accounts` call
(`POST /api/v2/buyer/storefront-accounts/sync`) can set its own optional
`payment_terms`. Each call targets one seller storefront
(`target.storefrontId`), so one request can carry different terms for
different sellers. Allowed values: `net_15`, `net_30`, `net_45`, `net_60`,
`net_90`, or `prepay`. Omit `payment_terms` to accept the seller's default
terms.
* **Organization level (`paymentTerms`).** One choice for the whole
organization, set with
[Update billing info](/v2/buyer/billing/tasks/update-billing-info) or the V3
`save_billing` tool. Allowed values: `net_15`, `net_30`, `net_45`, `net_60`,
or `net_90`. `prepay` is not an organization-level value. Apostra uses this
choice when it opens a consolidated-billing account for you; see
[Your organization's payment terms](#your-organizations-payment-terms).
* **Both are available only when requesting payment terms is enabled for your
organization.** Until then, a `sync_accounts` entry that sets `payment_terms`
fails with `PAYMENT_TERMS_NOT_SUPPORTED`, and `paymentTerms` is refused with a
validation error. The one exception is a storefront that routes accounts to
the seller's own sales agent, called through that storefront's AdCP
`sync_accounts`: it forwards `payment_terms` to the seller's agent unchanged
whether or not requesting terms is enabled, and the seller's agent decides.
* **The V3 hosted-agent tools cannot set per-seller terms.** `save_connection`
has no payment-terms field and there is no V3 `sync_accounts` tool. The only
payment-terms write in the V3 tools is the organization-level
`save_billing` `paymentTerms`. To request different terms from one seller, use
`sync_buyer_storefront_accounts` or its REST route.
AdCP lets a `sync_accounts` entry request `payment_terms`. The protocol does not
let a seller silently swap in different terms: the seller accepts the requested
terms or rejects the account. The seller sees the requested terms when it
reviews the account and can only accept or reject; it cannot edit them.
When requesting terms is not enabled for your organization, a request in which
any entry sets `payment_terms` is rejected as a whole with the AdCP error
`PAYMENT_TERMS_NOT_SUPPORTED`, which names the first such entry in `field` and
its requested value in `details.requested_payment_terms`. No account is created
or changed; retry without `payment_terms` to accept the seller's default terms.
Once requesting terms is enabled, the requested terms go to the seller with
the account request:
* **The seller reviews them.** Approving the account accepts the terms you
requested, and the account then reports them in `payment_terms`. There is no
counter-offer: if the terms don't work for the seller, the seller rejects the
account.
* **Only the net 60 default can be approved automatically.** A storefront may
approve some account requests without a person reviewing them. A request for
any terms other than `net_60`, shorter or longer, always waits for the seller
to review it, so it can take longer to approve.
* **You can change the terms while the request is pending.** Send the same
account again with different `payment_terms` and they replace the earlier
ones; that account's row returns `action: "updated"` and the seller reviews
the new terms.
* **A decided account keeps its decision.** Once the seller has accepted or
rejected the account, a request naming different terms changes nothing. That
account's row returns `action: "failed"` with the AdCP error
`PAYMENT_TERMS_NOT_SUPPORTED`, and the account keeps its status and its
terms. Other entries in the same request are unaffected. To change the terms
on an accepted account, agree them with the seller directly.
#### Your organization's payment terms
Your organization chooses the payment terms Apostra asks sellers for: `net_15`,
`net_30`, `net_45`, `net_60`, or `net_90`. Until you choose, `net_60` applies.
Set the choice with `paymentTerms` in
[Update billing info](/v2/buyer/billing/tasks/update-billing-info); it applies
to every advertiser in the organization. Like the rest of this section, it is
available once requesting terms is available.
Choosing terms is a go-live step for consolidated billing, so no
consolidated-billing buy is placed on terms nobody chose. Until an
organization administrator has chosen:
* buyer readiness shows a **Payment terms** check (`payment_terms`) that is
not complete, and it blocks go-live when you have sellers on consolidated
billing;
* a seller on consolidated billing shows `needs_commercial_setup`, and a buy
from it is refused with the `payment_terms` blocker; and
* direct-billed sellers are not affected.
If you have already connected a seller on consolidated billing, Apostra holds
that seller-account setup until you choose terms, then resumes it
automatically. Sandbox accounts are not held.
Choose terms explicitly, even to keep the default: send the default value
itself. Once chosen, the terms can be changed but not cleared, so
`paymentTerms: null` is refused with a validation error and the recorded
choice stays in place.
When Apostra opens an account for you with a seller (for example, when you
turn a seller on for an advertiser or buy from a seller for the first time),
the request asks for your organization's terms:
* **Longer terms can mean more rejections.** Sellers carry the credit risk on
their accounts, so some decline longer terms, and on storefronts Interchange
hosts or manages only the `net_60` default can be approved automatically.
* **Decided accounts keep their terms.** Changing your choice applies to
accounts opened afterwards. An account a seller has already accepted or
rejected keeps that decision and its terms; an accepted account keeps
working exactly as before.
* **A seller can refuse your requested terms.** The account request then
fails for that seller, and the connection reports that the seller does not
accept your terms. Review the seller's terms before explicitly requesting
an account on those terms, or leave that seller disconnected. Apostra does
not retry the request on the seller's default terms.
On the Storefront MCP discovery surface, an explicit opaque `account_id` must
belong to the authenticated buyer on that storefront. A missing buyer grant or
an ID belonging to another buyer returns the structured AdCP error
`ACCOUNT_NOT_FOUND` with `details.reason:
"account_not_authorized_for_buyer"`; it is not reported as a missing
storefront account context. Retrieve an authorized account with
`list_accounts`, or complete the seller's account-linking flow, before retrying.
Buyer Setup returns derived capability verdicts rather than a writable Account
mode. Before Organization proof, plan selection, and Terms, the Account can use
bounded demo and onboarding surfaces. After those steps it can perform real
non-spend work. Spend additionally requires the media-buying product entitlement.
Seller-direct transactions need nothing further from Apostra.
Consolidated-billing transactions additionally need your organization's billing
details: a payer name, street address, and country, so Apostra can invoice you.
They never need a card or an Apostra credit line, because Apostra takes no credit
risk on the seller's media. An organization that already has a verified card or an
Apostra credit line on file can keep buying while it adds its billing details.
Account holds block every capability.
Confirming or changing the buyer operator does not change your WorkOS
organization, sign-in, invitations, SSO, account membership, billing company,
advertiser, or brand domain.
## Destination go-live
Every seller or platform then shows only its own remaining requirement:
* A credential-free AdCP seller requires no connection step.
* An official adapter may require a free provider connection, account choice,
and advertiser mapping.
* A seller can be temporarily unavailable even when your connection is healthy.
* Direct-billed sellers and platforms bill the operator or advertiser and do
not use an Apostra rate card or payment method for that route.
* A seller on consolidated billing requires active media pricing supported by
the current pricing engine and your billing details before live spend. Add
them in **Plan & Billing → Payment & invoices**. Until they are complete, the
buy is refused with the `billing_details` blocker, readiness lists
`BILLING_DETAILS`, and `commercial.billingDetailsComplete` is `false`.
Readiness no longer reports `PAYMENT_AUTHORITY` or the `payment_authority`
blocker. Treat any blocker value you do not recognize as blocking. A child
account automatically uses the media pricing on its billing organization;
the account-delegation placeholder does not control that inheritance.
Once choosing payment terms is available, your organization must also have
chosen them; see
[Your organization's payment terms](#your-organizations-payment-terms).
The media rate card used to price a buy is separate from the Organization IU
Rate Card. The IU plan remains an invited staging pilot and is not a production
buyer-readiness requirement.
You can connect destinations, inspect accounts, and view mirrored campaigns
without charge. “Can buy now” means the applicable capability verdict is
allowed and at least one destination’s actual requirements are complete.
# Paying by card
Source: https://docs.apostra.com/v2/buyer/billing/concepts/pay-by-card
Save a card as your organization's payment method for eligible platform and IU charges
Adding a card gives your organization a payment method for eligible platform
and IU charges. You save it once on the
[Plan & Billing page](/v2/buyer/billing/plan-and-billing), and it becomes your
organization's payment method — there is no checkout step, no plan you must
pick first, and no commitment.
## What "add a card" does
When an org admin adds a card on **Settings → Plan & Billing → Payment &
invoices**:
1. The card details are collected in a secure form embedded in the page.
Card numbers are encrypted in transit and **never stored on Apostra
servers** — Apostra keeps only the card's brand, last four digits, and
expiry date, for display.
2. If your bank requires it, a quick verification step happens right in the
form (you may recognize this as the confirmation prompt your bank uses
for online purchases).
3. Once verified, the card becomes the organization's payment authority.
Saving it does not by itself accept an IU Rate Card, select a plan, or turn
on paid entitlements.
The card belongs to the **organization**, not to the person who added it.
Only org admins can add, replace, or remove it.
If you close or cancel the form before choosing **Save card**, no payment
method is added. You can return to **Payment & invoices** and start again.
## While card confirmation is processing
After you submit the secure card form, Plan & Billing shows **Card
confirmation in progress** with the submission time. This processing state is
saved for the organization, so it remains visible if you close the panel,
refresh, navigate away, or sign out and return later. You can safely leave the
page while Apostra waits for authoritative server confirmation.
Most cards confirm within seconds. If confirmation is delayed, Apostra checks
the payment processor directly so a delayed or missed notification can still
converge. While an unexpired confirmation is processing, Plan & Billing does
not offer a second indistinguishable card submission.
The processing attempt is not a payment method: it cannot be charged, grant
entitlement, or replace a verified card. When replacing a card, the existing
verified card remains active until the new card is confirmed.
The saved state then changes to one of these outcomes:
* **Verified** — the card's display-safe brand, last four digits, and expiry
replace the processing state.
* **Failed** — Plan & Billing explains that the card could not be confirmed
and offers **Try another card**. Apostra does not expose raw processor or bank
errors.
* **Expired** — confirmation did not reach a terminal result within 30 minutes;
Plan & Billing offers a fresh attempt.
If Plan & Billing cannot load the confirmation status, it shows **Unable to
load card confirmation status** with **Try again** instead of the add-card
form, because a card you already submitted may still be confirming. If you have
a saved card, it stays visible, but **Replace** is unavailable until the status
loads. If your session has expired, you are asked to sign in again.
## When the card is charged
Nothing is charged when you add the card. Automatic charging starts only when
your organization has an accepted IU Rate Card and the applicable charging
rollout is enabled. A plan commitment may be charged at cycle start; usage and
overage are charged after they accrue according to the accepted Rate Card. A
fee invoice records each issued charge. There are no card processing fees
added to what you pay.
If your organization would rather receive invoices and pay on net terms —
purchase orders, AP portals, bank transfer — that path is always available:
choose **Set up invoicing** instead. Both paths lead to the same Plan &
Billing page, and you can switch between them later. Larger organizations
often pick the card too: it skips vendor setup on both sides.
## If a payment fails
If a charge fails, Apostra retries automatically on a published schedule. It
emails the initial failure, failed-retry warnings, and the payment-hold notice;
there is no separate day-30 email in the current alpha:
| When | What happens |
| - | - |
| Right away | Email: the payment failed, with a link to update your card |
| 3 days later | First automatic retry |
| 7 days later | Second (final) automatic retry, with an email warning that an unresolved payment hold will be recorded soon |
| 10 days later | If still unresolved: Apostra records and displays a payment-failure hold. The current alpha does not yet enforce that hold in spend, campaign, subscription, or entitlement authorization. |
| 30 days later | If still unresolved: the dunning case is marked lapsed. The current alpha does not automatically downgrade the organization to the free tier or add a new enforced restriction. |
The hold is currently a billing-status and operations signal, not an
authorization gate. It does not pause new intelligence-credit spend, media
commitments, subscriptions, or upgrades. The intended future restriction model
would preserve access to:
* Your data, reports, and dashboards
* Data export
* Media that is already delivering (a card hiccup never pauses delivery
mid-flight — that would create make-goods and harm the seller)
* This page and updating your card — the fix-your-card flow is never
gated by the problem it exists to fix
**Payment recovery is self-serve.** Update your card, then either wait
for the next automatic retry or click **Update card & pay now** to retry
immediately. A successful charge resolves the dunning case and removes its
recorded hold; because the current alpha does not enforce that hold at
authorization call sites, this is payment-state recovery rather than access
restoration.
The timings above come from the card-rail's global dunning configuration; they
are not adjusted per account today.
## Updating or removing the card
* **Replace** — add the new card the same way; it becomes the payment
method and the old one is discarded. Do this any time, including before
the old card expires.
* **Expiry** — when your bank reissues a card, the saved card usually
updates automatically with no action from you. When it can't, Plan &
Billing shows an "expires soon" badge and you get an email about 30 days
before the expiry date — a heads-up, not a restriction: an expiring card
with no failed charge never blocks anything on its own.
* **Remove** — allowed when nothing depends on the card. If it is your
organization's only way to pay, removal is blocked with a pointer to add
a replacement first (or set up invoicing). This preserves a valid payment
path; card removal does not itself change entitlements.
## For agents
The saved payment method appears in the
[billing account document](/v2/buyer/billing/tasks/get-billing-account) as
`payment.paymentMethod` — kind, brand, last four digits, expiry, and status.
When an organization has no payment method and needs one, the document's
`nextAction` is `ADD_PAYMENT_METHOD`. Card entry itself is always a human
step (it happens in a secure form), so agents hand that moment to a person —
and there is a dedicated task for exactly that handoff.
### Requesting a card via your agent (`add_payment_authority`)
An agent working on the organization's behalf requests a payment method with
the `add_payment_authority` MCP task, in three steps:
1. **Request** — call with `action: "request"`. The task stages the request
and returns a single-use `confirmationToken` (valid 5 minutes). Nothing
is created yet.
2. **Confirm** — call again with `action: "confirm"` and that token. The
task returns `{ method: "capture_link", url, status, expiresAt }` — a
link valid for 30 minutes that serves one verified card capture.
The URL is returned exactly once; the agent saves it and hands it to the
person who holds the organization's card. This legacy URL can be reopened
until the capture is verified or expires; it is not a one-time browser
exchange.
3. **The cardholder opens the link** in any browser — no account or sign-in
is needed; the link itself is the authorization. The page shows which
organization requested the card and the same secure embedded form as
Plan & Billing. Card details go directly to the payment processor and
never pass through the agent or Apostra API.
The agent then polls `action: "status"` (every 15–30 seconds is plenty)
until the status is `verified` or `expired`. A verified capture is historical
evidence: read the billing account's current `payment.paymentMethod` before
reporting an active payment authority. If the capture is expired, the legacy
task requires a fresh request. The link statuses are
`pending → opened → verified`, with `expired` as the terminal
didn't-complete state.
For a private Partner Offer, this is the MCP continuation after the
organization administrator accepts the exact Offer in `open_iu_plan_task`.
Offer acceptance, verified card authority, and Partner operating authority are
separate facts. After a verified capture, read the Partner access decision:
`purchased: true` proves the contract-backed entitlement, while `allowed: true`
also requires the organization’s owner claim, registration, current production
certification readiness, usable payment standing, and no hold. Never send PAN,
CVC, or a card token in an MCP tool argument.
### V3 agents (`save_billing`)
On `/mcp/v3`, call `get` with `{ "kind": "billing" }` to read the selected
organization's billing readiness, current Terms and payment status in either
text or structured output. It omits card details and browser credentials.
**The billing read does not add a secure one-time cardholder handoff.**
`paymentAuthority.secureHandoff.available` is `false`. Existing `save_billing`
integrations retain their `request`, `confirm` and `status` actions, permissions
and response shapes. The legacy browser credential can be reopened until
verification or expiry; it does not provide a one-time exchange with fresh
human proof. Its continued availability does not complete the new secure
text-only setup flow.
Poll current readiness by repeating the billing read every 15–30 seconds.
`paymentAuthority.status` and `paymentAuthority.expiresAt` describe the latest
capture link, at the same path as the `save_billing` status poll; `submission`
describes a submitted card form, including
`processing`, `verified`, `failed` and `expired` outcomes. Only `paymentAuthority.verified`
reports whether the canonical billing account currently has a verified payment
method. A request, an opened browser page, and an old verified capture are not
equivalent to that current state.
Existing `save_billing` calls with
`{ "paymentAuthority": { "action": "status" } }` retain their capture-history
response: `method`, `status` and `expiresAt`, with the existing
`payment_authority_pending`, `payment_authority_opened`,
`payment_authority_verified` or `payment_authority_expired` action. Existing
permissions still apply, and no capture record returns `NOT_FOUND`.
From a Buyer or Seller account, authorized organization admins can accept the
current Terms through `save_billing` and verify the result with another billing
read. Parent-container sessions can read readiness, but retain their existing
write restriction; `nextAction.canCompleteInChat` is `false`. Follow the
[text-only billing flow](/v2/setup/v3/tool-reference#plan-and-billing) for the
read/write/read sequence and status fields. The new readiness flow stops before
card setup. A versioned one-time handoff with fresh cardholder confirmation is
still needed. Existing capture-link clients continue to use their deployed
contract.
Behind the legacy capture page sits one public endpoint,
`GET /api/v2/capture-links/{token}`: the hosted page exchanges the link
token for the embedded form's credentials (and marks the link opened), and
polls it with `?view=status` while verification completes. The token is the
sole authorization — unknown tokens return nothing about any organization,
and requests are rate-limited.
Requesting a link requires org-admin access or an org-scoped service token;
the task is available on both the buyer and legacy v2 `storefront` MCP servers because
Plan & Billing is org-level regardless of role.
Two more `nextAction` values cover the failure/recovery path above:
* `RESOLVE_HOLD` — a payment-failure hold is recorded (day +10 in the table
above). The current alpha does not enforce it as an authorization gate.
Resolved by `POST /api/v2/billing/payment-methods/pay-now`, which retries
the outstanding charge on the org's saved card immediately. The route
returns `processing`; poll the billing account document and watch
`nextAction`/`standing.holds` for confirmation rather than inferring
success from the response (the actual recovery is confirmed server-side,
asynchronously).
* `UPDATE_PAYMENT_METHOD` — a soft nudge shown when the card is inside the
expiry lookahead window. It never gates anything on its own; it resolves
the same way as `ADD_PAYMENT_METHOD` (replace semantics).
`standing.holds` on the billing account document lists any active holds —
today only `PAYMENT_FAILED` — each with a `reason` and `createdAt`.
## Related
Where the card is added and managed
The document that carries the payment method and next action
# How IU billing works
Source: https://docs.apostra.com/v2/buyer/billing/how-iu-billing-works
The whole path from a published price to an invoice you can check — with worked examples and a plan calculator
This page is the walkthrough: what happens between a published price and a charge on
your account, what you do at each step, and **what you can check afterwards**. If you
only want the price list, the
[Organization IU Rate Card](/v2/buyer/billing/organization-iu-rate-card) has it.
The IU plans and prices below are agreed and published for a controlled
staging pilot. They are **not published in production**, so no organization
needs to accept them in production today and no IU charges are being made.
Existing accepted media-pricing arrangements are unchanged. When your
organization is invited, your confirmation shows the exact version, term, list
price, and effective price that would apply to you.
## The short version
* **Connecting costs nothing.** Connecting an account, discovering it, and mapping it
to an advertiser never consume IUs.
* **You pay for successful work**, priced by an activity term on the Rate Card you
accepted. Attempts, retries, and our own failures are free.
* **One plan for the whole organization.** Buyer activity, Seller Account work, and Murph
draw from the same plan and the same wallet.
* **A commitment lowers the per-IU price.** Usage beyond your included IUs bills at
the metered rate.
* **Every charge traces back to a term you accepted**, and you can read that trail
yourself — see [checking our numbers](#checking-our-numbers).
## The seven steps
Apostra publishes an immutable Rate Card revision: plan sizes, monthly
commitments, included IUs, activity prices, currencies, and an effective date.
Publishing a revision does not commit anyone to anything.
**You do:** nothing.
Your organization gets one **Effective Rate Card** — the complete commercial
result for you. It starts from a published plan and can include one authorized
adjustment: a corporate discount, a promotion or referral code, a negotiated
private package, or a customized Offer based on an exact Rate Card plan.
**You can check:** the list price, the adjustment, and the effective price appear
side by side before you confirm. Discounts do not stack, and only one complete
offer is effective at a time.
Only a verified organization administrator can accept a plan. An agent, API key,
service account, or support session cannot make that commitment on your behalf.
Acceptance snapshots the exact agreement, revision, plan, adjustments, prices,
actor, and time into an immutable **acceptance record**. A later price change never rewrites
it — changes arrive as a new offer you can review.
**You do:** review and accept, decline for that revision, or decide later. All
three are recorded; none of them is assumed.
Either save a card, or be set up for invoicing on your existing terms. This is
separate from accepting a plan: accepting a plan does not by itself start
charging, and charging is switched on per organization, never silently.
**You do:** add payment details in **Settings → Plan & Billing**.
When a capability has a published IU price, successful use draws from your plan.
Your wallet shows the balance, what consumed it, and a projection for the rest of
the period.
Usage draws from expiring credit lots first, then rolled-over IUs, then the
period's included allowance, then overage.
**You can check:** usage appears on **Usage & credits** as it happens, attributed
to the activity that caused it.
The activity list and daily trend are measured in IUs and reconcile to the
wallet's **IUs used** total. They do not come from Apostra's cost to run an AI
provider. A capability can incur an internal provider cost without consuming
a priced IU activity; in that case it does not appear as customer IU usage.
Each charged month produces one numbered fee invoice: plan commitment, overage as
IUs × your rate, and payments applied. Commitment and overage are switched on
separately, so an invoice can carry the commitment while overage is still only
accruing — see [billing cycle and fee
invoices](/v2/buyer/billing/organization-iu-rate-card#billing-cycle-and-fee-invoices).
**You can check:** invoice numbers are sequential with no gaps, and an issued
invoice's charges never change. Payments are appended as they settle.
The invoice, the usage that produced it, and the term that priced it are the same
three records we reconcile internally. Nothing on your invoice should be
unexplainable from your own reads.
**You can check:** see [checking our numbers](#checking-our-numbers).
## What consumes IUs, and what never does
**One thing consumes IUs:** successful activity that has a published price on the Rate
Card your organization accepted.
New Rate Cards use the three published v4 activities on the
[Organization IU Rate Card](/v2/buyer/billing/organization-iu-rate-card#complete-activity-schedule):
Brief response, Enhanced Reporting, and Apostra media buy. Older
accepted Rate Cards retain their own immutable activity terms.
Everything below is always free, and stays free whether or not you have a paid plan:
* connecting an account or an ad server;
* discovering accounts and mapping them to advertisers;
* reading, reporting, and exporting your own data, unless you enable Enhanced
Reporting for a connected account;
* approving something we proposed;
* retries, routing, cache hits, and no-op work;
* anything that failed; and
* work caused by an Apostra defect.
The order of operations is fixed: **zero-rate first, then credits, then bill.** If it
is ambiguous whether something should be charged, it resolves free.
### The v4 activity schedule
New integrations can read the current three-activity catalog from
`GET /api/v2/billing/iu-rate-card/v4`. The response carries both the accepted
`activityTerms` and the resolved `activitySchedule` so a client can show the exact
price and boundary that govern the organization:
| Activity code | Customer-facing activity | Starting rate and boundary |
| - | - | - |
| `COMPLETED_INTERCHANGE_MERCHANDISING_CYCLE` | Brief response | 1 IU for one completed Apostra merchandising cycle |
| `SOCIAL_ACCOUNT_INTELLIGENCE_BILLING_PERIOD` | Enhanced Reporting | 4 IUs for one connected account in a billing period after its account-level Enhanced Reporting control is enabled and that account's existing reporting subscription successfully syncs |
| `INTERCHANGE_MEDIA_BUY_BILLING_PERIOD` | Apostra media buy | 1 IU for one qualifying non-social media buy in a billing period with positive impressions or spend |
The unversioned, `/v2`, and `/v3` reads remain compatibility boundaries for clients
that use the older activity vocabulary; they do not add the v4 rows. Opt in to
`/v4` before depending on these codes. An accepted Rate Card still freezes its
catalog version and resolved prices, so reading the newer endpoint never rewrites
an earlier agreement.
For Apostra media buys, the latest authoritative delivery totals control the
result. If both impressions and spend become zero before invoicing, the provisional
IU is removed. After invoicing, the same correction produces an auditable one-IU
credit or reversal against the original period. A later positive correction can
reinstate that same unit once, but cannot create a second unit for the buy and
period. See the [complete activity schedule](/v2/buyer/billing/organization-iu-rate-card#complete-activity-schedule)
for all qualification details.
## Plan shapes
| Plan | Monthly commitment | Included IUs | Price per included IU | Overage |
| - | -: | -: | -: | -: |
| Pay as you go | \$0 | 0 | — | \$5.00 / IU |
| 250 | \$1,000 | 250 | \$4.00 | \$5.00 / IU |
| 1,000 | \$3,000 | 1,000 | \$3.00 | \$5.00 / IU |
Two break-even points fall out of those numbers, and they are worth knowing before
you choose:
* Above **200 IUs a month**, the 250 plan costs less than pay as you go.
* Above **650 IUs a month**, the 1,000 plan costs less than the 250 plan.
On a committed plan, unused included IUs carry into the next monthly period only, up
to **50% of that next period's included quantity** — at most 125 IUs on the 250 plan,
500 on the 1,000 plan. Carried IUs expire after that one period and never accumulate
again. Pay as you go has no allocation to carry.
**This is the standard list, not everyone's price.** Your Effective Rate Card is the
authority. An authorized corporate discount, a promotion or referral code, a
negotiated private package, or a customized Offer replaces these figures, and Plan &
Billing shows the list price and the effective price side by side before you confirm.
If your organization has been quoted something else, that quote — once it is an
Effective Rate Card you accept — governs your charges, not this table.
**Prices are set per currency, never converted.** The figures above are the USD list.
A Rate Card revision carries a separately published price for each supported
currency, so a plan in EUR, GBP, or AUD is **not** the USD figure at an exchange rate
— it is its own published number. Your organization's currency is fixed on the
revision you accept, and your invoice, charge, and wallet all use it. Read the
numbers on your own card, not these, if your card is not in USD.
**And these are today's standard figures.** This table describes the standard model as
agreed on 2026-07-22. Standard prices can change, and when they do they arrive as a
**new published revision** — never as an edit to a revision anyone accepted. Your
acceptance record snapshots the exact version, term, and prices you agreed to, so a change to
this table does not change what you are paying: you would see a successor offer and
decide whether to accept it.
## Calculator
Enter the IUs you expect to use in a month. The cost of each plan is shown, with the
cheapest one marked.
This computes the **standard USD list** and is not a quote. If your organization has a
corporate discount, a negotiated package, a customized Offer, or a card in another
currency, put the numbers from your own Effective Rate Card into the formula below —
prices are published per currency rather than converted from USD.
The formula, if you would rather compute it yourself or have an agent do it:
```
cost = commitment + max(0, used − (included + carried_in)) × 5.00
```
`carried_in` is the smaller of last period's unused included IUs and half of this
period's included quantity.
## Worked examples
Uses 12 IUs in the month.
```
commitment $0
metered 12 × $5.00 $60.00
──────────────────────────
invoice $60.00
```
No commitment, no included allocation, nothing to roll over. Every IU is \$5.00.
Committed to 250 IUs for \$1,000. Uses 310 IUs, with nothing carried in.
```
commitment $1,000.00
included 250 IUs — covered
overage 60 × $5.00 $300.00
────────────────────────────────
invoice $1,300.00
effective per IU $4.19
```
Because they are on a card, the \$300 of overage is charged when it crosses the
account's threshold during the month rather than arriving as a surprise at close.
The remainder settles when the cycle closes.
On the 250 plan. Period 1 uses 180 of 250 IUs.
```
period 1 used 180 of 250 unused 70
carry min(70, 125) = 70 IUs into period 2
```
Period 2 has 250 included + 70 carried = 320 IUs before any overage. If they use
300, the invoice is the \$1,000 commitment and no overage. The 20 IUs left over
from the carried lot expire — carried IUs do not carry again.
A seller billing organization receives one **100-IU credit, valid for 60 days**,
on its first IU plan acceptance. You do not have to be a new customer — if you
have been with us a while and are accepting an IU plan for the first time, the
credit is yours too.
It can be spent on **any IU-priced activity**. The window is a fixed 60 days from
your signup commit, not a number of billing periods, so it does not shift with
your billing cycle and you never need to know where your cycle boundary falls.
**Usage & credits shows the exact expiry date.**
One credit per billing organization: later Seller Accounts do not create another. It
does not roll over, has no cash value, and does not stack.
## Reading your invoice
Every charged month produces one numbered fee invoice on **Settings → Plan & Billing
→ Payment & invoices**, showing:
* the plan commitment, at the price fixed in your accepted terms;
* overage as a quantity of IUs multiplied by your plan's rate;
* payments applied, as they settle.
Two properties are worth relying on. **Invoice numbers are sequential with no gaps**,
so you can tell whether you have seen every invoice. And **an issued invoice's charges
never change** — a correction arrives as its own document, never as an edit to one you
have already received.
Organizations paying by card are charged automatically. Invoiced organizations receive
the fee invoice only and pay on their existing terms; no card charge is made.
### When a card payment settles before the cycle closes
A card payment can succeed during the month before its fee invoice is issued. The
payment stays attached to that exact billing cycle and is recorded as deferred until
the cycle closes. A retry or **pay now** recovery replaces the failed attempt with the
successful payment on the same cycle; it does not create a second cycle or detach the
payment from its usage.
When the cycle closes, Apostra issues its invoice from the frozen usage drawdown and
accepted plan terms, including payment applications for successful charges. Collected
payments are then recognized as revenue. Separately, Apostra's billing checks compare
those records with successful charges and invoice arithmetic for differences. If
charging is turned off after a payment was collected, no new charge is made, but that
paid cycle still closes and the accounting checks continue so the payment is not left
without an invoice.
For day-to-day billing, nothing changes: finance teams can use the numbered invoice and
its applied payments as their audit trail, while smaller teams can continue using Plan
& Billing without a new setup step.
## If a payment fails
A published recovery ladder applies: automatic retries on a schedule, email notices,
and a self-serve **pay now** action that clears the failed-payment status as soon as it
succeeds.
Plan & Billing reports an unresolved dunning-backed hold; reaching the account's cap
records a separate internal hold. In the current alpha, neither hold is consulted by a
production authorization path, so it does not automatically block new paid platform
activity. **Viewing your data, reporting, exporting, and fixing your payment method
remain available.**
## Checking our numbers
You should not have to take an invoice on trust. These reads are the same records the
charge was built from:
| To check | Read |
| - | - |
| The exact v4 terms you accepted | `GET /api/v2/billing/iu-rate-card/v4` |
| Your plan, standing, and payment methods | `GET /api/v2/billing/account` |
| Your invoices and their line items | `GET /api/v2/billing/fee-invoices` and `/fee-invoices/{invoiceNumber}` |
| Invoice activity as it happens | `GET /api/v2/billing/invoice-activity` |
Authenticate with `Authorization: Bearer $SCOPE3_API_KEY`. Agents should use the typed
`open_iu_plan_task` and `get_billing_account` tools rather than reconstructing
this from generic calls.
The reconciliation you can do yourself: **the IU quantity on an invoice line should
equal the usage attributed to that activity for the period, priced at the rate on your
accepted terms.** If those three do not agree, that is a defect on our side — tell us and we
will explain the difference rather than adjust the invoice quietly.
## Two bills, kept separate
IUs price platform activity. Media spend and its accepted contract fee terms are a
separate commercial stream on a separate bill. Plan & Billing can show both, because
an organization may owe both, but neither replaces or silently amends the other.
## Related
The price list, adjustments, rollover, and acceptance in detail
The surface where you review terms, usage, and invoices
Saving a card, thresholds, and the recovery ladder
How media spend is invoiced, and where remittance details come from
# Media billing entity configuration
Source: https://docs.apostra.com/v2/buyer/billing/media-billing
Configure and preview the legal entity intended for consolidated media invoices
Media billing entities let an organization configure and preview the legal
recipient intended for a **consolidated billing** media invoice. They do not
decide whether Apostra invoices a transaction: that comes from its billing party
(`agent` vs `operator` vs `advertiser`).
This surface is currently configuration and preview only. You can create
entities, attach advertisers/accounts, and inspect the most-specific match,
but the media-invoice producer does not yet consume that resolution. Creating
or attaching an entity does not by itself change invoice issuance or delivery.
## Two streams, kept separate
* **Apostra fees** — platform fees and IU bundles/overage when your
organization's commercial terms enable them.
* **Media billing** — legal-recipient configuration for media transactions
whose billing party is `agent`. This page covers that preview model.
A surface, invoice, or line item is always identifiably one stream or the
other — never a blended "billing." See
[Plan & Billing page](/v2/buyer/billing/plan-and-billing) for where each
stream renders.
## The resolution hierarchy
Media billing entities are configured at up to three levels and resolved
**most-specific-first**. The presence of an entity at a level is what makes the
preview resolver select it:
| Level | Rule |
| - | - |
| **Advertiser** | If an advertiser has its own billing entity attached, the preview resolves to that entity. |
| **Account** | Else, if the advertiser's child account has an attached entity, the preview resolves there. |
| **Organization** | Else, the preview uses the organization's primary entity when one exists; without one it returns `resolvedVia: "none"`. |
"Most specific wins": an advertiser's own entity beats its account's, which
beats the organization default.
## Country is metadata, not a routing key
A billing entity's country is stored identity metadata. Currency is configured
independently; this preview does not derive tax treatment, currency, or address
format from country. **Country never participates in resolution.** Matching is
purely structural (advertiser → account → organization); it never looks at
where media delivered or where an advertiser operates.
To preview different entity matches by market, model each market with a
**market-scoped advertiser** (for example, "Acme Beverages South Africa") and
attach it to the corresponding configured entity. This matches how agencies
structure market P\&Ls without introducing a separate geo-resolution key. The
invoice producer does not consume this preview yet.
## Entities
A media billing entity carries:
* **Entity name** — the legal entity's name
* **Country** — stored ISO 3166-1 alpha-2 identity metadata
* **Currency** — ISO 4217 currency stored for the configuration preview
* **Billing email(s)** — invoicing contact
* **Address** — street, city, state/region, postal code
* **Tax ID** — optional (e.g. VAT number, EIN)
When an organization has entities, exactly one can be **primary** as the
organization-level fallback. Your first entity automatically becomes primary;
promoting a different entity demotes the previous one in the same step. The
current product does not require an entity before a buy and can resolve to
`none` when the organization has not configured one.
## Attachments
An attachment associates one advertiser or one account with a specific entity
for resolution preview — exactly one of the two per attachment.
## In the product
The **Media billing** tab of the
[Plan & Billing page](/v2/buyer/billing/plan-and-billing) is where this model
renders. It appears for organizations with buyer capability (a pure seller
can have platform fees and payouts, but not media-billing configuration) and shows:
* **Billing entities** — your entities as cards: name, country, currency,
billing email, and an "Org default" tag on the primary. An entity missing
its tax ID is flagged. When you have no entities yet, the tab explains that
no entity exists yet and offers the setup action.
* **Resolution** — one row per attachment (advertiser or account → entity),
plus an "Everything else" row showing the organization default when one
exists.
* **Which configured entity matches?** — an inline inspector: pick any advertiser or
account and see the three resolution steps (advertiser attachment, account
attachment, organization default) with the matching step highlighted, then
the resolved entity with its country and currency—or `none` when nothing is
configured.
The tab writes through the same admin-gated API documented below. UI and API
share the same configuration rules; neither path is wired into invoice
production yet.
## For agents
`GET /api/v2/billing/media-entities`
`POST /api/v2/billing/media-entities` (admin)
`PUT /api/v2/billing/media-entities/{entityId}` (admin)
`DELETE /api/v2/billing/media-entities/{entityId}` (admin)
`GET /api/v2/billing/media-entities/attachments`
`POST /api/v2/billing/media-entities/attachments` (admin)
`DELETE /api/v2/billing/media-entities/attachments/{attachmentId}` (admin)
`GET /api/v2/billing/media-entities/resolve?advertiserId=` or `?childCustomerId=`
The resolve endpoint previews which configured entity matches an advertiser or
account: it returns the resolved entity plus `resolvedVia`
(`advertiser` | `account` | `org`), or `resolvedVia: "none"` when your
organization has no entity at all yet. Admin-gated writes are enforced
server-side, so any future client shares the same rule as the REST API.
Media billing always belongs to the organization. A request made while you work
in one of the organization's accounts reads and changes the organization's
entities and attachments, never a separate set for that account, and it
requires you to be an administrator of the organization itself. Administering
only the account is not enough. Entities and attachments an account configured
for itself before this still show there, are updated or removed from there, and
take precedence for that account: its own primary entity is used before the
organization's, and the preview reports it as `resolvedVia: "account"`. New entities and attachments always belong to the organization.
## Related
Where interchange fees and media billing both render
How invoicing and remittance work
# Organization IU Rate Card
Source: https://docs.apostra.com/v2/buyer/billing/organization-iu-rate-card
How one Effective Rate Card defines pricing, access, rollover, and activity terms across your organization's workloads
The Organization IU Rate Card is the price list your organization will use for
Intelligence Unit (IU) activity across Apostra. It applies once at the
billing-organization level: buyer activity, Seller Account work, Murph, and other
IU-rated capabilities draw from the same plan and wallet.
The standard IU Rate Card is published in staging for a controlled pilot and
visible only to invited organizations. It remains unpublished in production
until the pricing and governing terms are approved, so no organization needs
to accept it in production today. Invited staging organizations can review and
accept the exact effective revision. For invited organizations, Plan & Billing
may show the accepted included-IU allocation, observed usage, and a display-only
forecast. The accepted Rate Card has the complete current activity schedule.
In the invited pilot, an admitted published activity may move the displayed
IU balance and appear as usage, while monetary charging, invoices, payment
collection, and renewal charging remain off. Entitlement enforcement remains
separately controlled. Existing accepted media-pricing arrangements are
unchanged. An IU Rate Card is not a prerequisite for buyer setup or media
buying outside the invited pilot.
## The simple model
* **Connect at no cost.** Connecting an account, discovering it, and mapping it
to an advertiser do not consume IUs.
* **Pay for IU-rated use.** When a capability has a published IU price, its
usage draws from your organization's one IU plan and wallet.
* **Commit for a volume discount.** A monthly commitment lowers the effective
price of the included IUs. Overage uses the metered rate.
* **Choose the package that matches the relationship.** A Rate Card plan can
combine pricing and IUs with contracted product rights, premium support, and
proposal defaults while the same wallet continues to price metered usage.
There is no separate buyer, Seller Account, Murph, or BYOBA plan. BYOBA—bringing or
building your own buyer agent—is simply one way to use the platform.
## Buying with Apostra
Advertisers can connect media accounts, create campaigns, and place buys for
free. Apostra takes 0% of media. You can work in Apostra, through your own
agent, or in your chat of choice. Availability depends on the accounts,
integrations, and sellers that support the request.
Optional Intelligence for planning, recommendations, and enhanced reporting is
currently in beta. The activity catalog sets Enhanced Reporting at 4 IUs per
connected account per billing period, although every plan in the current
template waives it. Customer-facing plan pricing for other buy-side
Intelligence activities is coming soon. Apostra shows the resolved price before
you opt in, and eligible activity draws from one IU wallet shared across your
organization.
Enterprise is a contact-us offer. A scoped agreement can include the Apostra
team operating Apostra for you, custom integrations, cooperative agent
development with forward-deployed engineers, dedicated human support, and
enterprise governance controls. The proposal or statement of work defines the
services, responsibilities, support commitments, and price. Ordinary product
support through Murph remains part of the product.
Connect & Buy covers one Buyer Account. **Business Manager** is for agencies
and teams that run many brands: it covers up to 100 Buyer Accounts under one organization, on one IU bill. It is a
private plan, offered through a Commercial Offer.
These are buy-side choices, not Seller Account plans. The Effective Rate Card
governs any IU activity your organization accepts, while a separate Enterprise
agreement governs contracted services.
## One Effective Rate Card
The Rate Card shown for acceptance is the complete commercial result for your
organization. Each plan on an exact published Rate Card revision is a complete
commercial package: its pricing and IU allowance, entitlements, support, and
proposal defaults travel together. It can incorporate one authorized adjustment
source:
* a corporate percentage adjustment;
* a promotion or referral code;
* a negotiated private package, such as 500 IUs for \$500; or
* an explicitly customized version of one published plan.
These are not separate terms scattered across the product. A Commercial Offer
or code distributes the terms; the resulting **Effective Rate Card** is what
you review, accept, and later see in Plan & Billing. It is versioned and
snapshotted with the acceptance record so a later edit cannot rewrite what you accepted.
Entitlements are enforced separately from billing, but the access bundle
and support included in a plan appear beside its price. A new Offer selects one
exact Rate Card revision and plan. It either uses the standard plan terms or
records an explicit customization with its reason and exact differences. The Offer
snapshots the resolved bundle revision, every canonical feature key it contains,
any premium-support package, and the resolved proposal. A later plan or bundle
revision cannot change an Offer already issued or a term already accepted.
Draft revisions also preserve their commercial lineage. A successor identifies
the published revision it carries forward; a deliberate fresh start records why
no predecessor terms apply. If an operator abandons an unpublished draft, Apostra
retains its terms, lineage, actor, timestamp, and reason as immutable audit
history. An abandoned draft is never published, offered for acceptance, or used
to price activity, and it does not alter any revision your organization accepted.
## Choosing a seller plan
For organizations enrolled in IU plan selection, the plan chooser groups seller
plans under **Listing** and **Merchandising**. Listing choices include **Listing** and **Listing +
Distribution**. Merchandising choices are the Agentic Media Company plans:
**Storefront agent** and **Storefront agent + Distribution**. The displayed price and included access always
come from the published revision offered to your organization. A plan name
does not set a price or change your accepted terms.
The available choices follow your organization's active products. A seller that
only lists its Sales Agent sees Listing choices. A seller whose storefronts are
all on Agentic Media Company can see both groups, because a Listing plan also
covers an Agentic Media Company storefront. A public plan for an organization
with several products must cover all of them, so an organization that also
holds Buyer Accounts or a sales agent offering sees only Merchandising choices.
If no public plan fits, the chooser offers **Get in touch**.
Your plan also decides which accounts the organization can hold:
| Plans | Accounts they hold |
| - | - |
| Buyer plans: Connect & Buy, Business Manager, Enterprise | Buyer Accounts only. Connect & Buy holds one; Business Manager holds up to 100; Enterprise holds what its terms state |
| Listing plans: Listing, Listing + Distribution, Enterprise — Listing | Seller Accounts only, on either seller product |
| Global Market Maker | Seller Accounts on either seller product, together with Buyer Accounts and a sales agent offering |
| Certified Agent Developer | A sales agent offering only |
| Agentic Media Company plans: Storefront agent, Storefront agent + Distribution, Enterprise — Storefront agent | Seller Accounts on Agentic Media Company, together with Buyer Accounts and a sales agent offering |
Adding a Buyer Account to a Seller organization on a Listing plan, for example,
needs an Agentic Media Company plan, and the change is refused until the
organization has one. Global Market Maker is the one exception: a private seller
plan that also covers Buyer Accounts and a sales agent offering, even when the
storefront is Listing only. See
[Which plan holds which accounts](/v2/setup/account-access#which-plan-holds-which-accounts).
**Enterprise — Listing** and **Enterprise — Storefront agent** are contact choices
for the applicable Listing or Merchandising group. They have no public price and cannot be accepted
as a zero-priced plan. Any private Commercial Offer prepared for your organization returns
to this same chooser for review and acceptance.
## Getting help with a plan
Select **Get in touch** in plan selection, including when public plans are
available. You can also open the chooser from **Plan & Billing**. A billing
administrator must send the request from their own account. Staff impersonation
and service tokens cannot send it on your behalf.
Add an optional message of up to 1,000 characters and select **Send request**.
Replies stay in the app. Your request is tied to the plans you were shown and
the plan choice you asked about. It does not accept a plan, change pricing or
access, or promise a response deadline. Retries of the same current request
are attached to the same commercial request.
After **Request received**, follow progress in **Your requests**, under **Help**
in Apostra, when tracking is available. If tracking is temporarily
unavailable, keep the reference when asking for an update. **Back to plans**
reloads the current offer. If the available plans changed, no request is sent.
Review the current plans, then send again.
During signup, **Get in touch** explains how to continue after account creation.
Nothing is sent anonymously. Finish creating your account, then send the request
from **Plan & Billing**. Seller signup does not ask for your seller product
separately. It shows every public plan that applies to a Listing-only Seller or
to an Agentic Media Company, and the plan you accept sets your starting product.
A Listing plan, or no paid plan, starts you as Listing only. A Storefront agent
plan starts you as an Agentic Media Company. You can change the product later
from Settings.
A paid plan you choose while creating a Seller account is accepted as part of
that signup, once your email address is verified, even though your Seller Storefront
is created a moment later in the same signup. The plan takes effect when signup
completes. Only an accepted plan sets your starting product. If your email is
not yet verified, or the plan you picked is no longer offered when you finish
(for example because the Rate Card changed while you were signing up), signup
finishes on the free pass-through terms as Listing only and asks you to choose a
plan again. To choose a Storefront agent plan after that, first switch to an
Agentic Media Company in **Settings → How would you like to sell?**.
## Rate Card plans
Rate Card revisions can contain public plans and private negotiated packages.
The seller templates use **Listing**, **Listing + Distribution**,
**Storefront agent**, and **Storefront agent + Distribution**. A template is an
unpublished starting point: an operator must approve and publish an exact
revision before it can be offered. Buyer and other product plans may also
appear when they cover your organization’s products.
Saving, enabling, and evaluating AI Business Rules is not an IU-rated activity.
Every plan in the current template waives Enhanced Reporting. Listing paths also
waive brief responses and qualifying non-social media buys. Merchandising
paths—the Storefront agent plans—keep the catalog's 1-IU defaults for brief
responses and qualifying media buys. The currently published staging standard
list remains **Pay as you go**, **250**, and **1,000** until a successor Rate Card
is published. Existing accepted plans remain readable and enforceable. The
commitment, included quantity, overage rate, activity terms, product rights,
support, and proposal defaults belong to the selected plan.
The standard USD commitments, included quantities, overage rates, and break-even
points are on one page — **[How IU billing works](/v2/buyer/billing/how-iu-billing-works)** —
along with a calculator and worked examples. That page is the standard USD list;
this page is about the exact revision and plan that govern *your organization*.
Usage only provides no included monthly IU allocation. Once production
billing is enabled, commitments will be billed at the start of each monthly
period; metered usage and committed-plan overage will be billed after the
period.
The standard sizes are **not yet published in production**, so they are not a
production offer today — they are available only where an effective Rate Card has
been published in your environment and shown to your organization. Your
confirmation always shows the exact version, term, list price, and effective
price that would apply to you in that environment. The same Rate Card includes the
complete activity schedule; several rows remain under calibration and are
marked non-billable below.
**Whatever the standard list says, your Effective Rate Card is the authority.** An
authorized corporate discount, a promotion or referral code, a negotiated private
package, or a customized Offer replaces the standard figures, and Plan & Billing
shows the list price and the effective price side by side before you confirm. If
your organization has been quoted something else, that quote — once it is an
Effective Rate Card you accept — is what governs your charges. Prices are also published per
currency rather than converted, so a card in EUR, GBP, or AUD carries its own
published numbers.
## Pricing adjustments and private offers
If Apostra has authorized a corporate discount for your organization, it applies
once across the plan—not separately to buyer and Seller Account activity. Before
you confirm, Plan & Billing shows the list price, discount, and effective price.
Discounts do not stack.
A negotiated package is shown as its own private Effective Rate Card, not as a
second hidden discount. For example, an Offer for 500 IUs at \$500 shows that
commitment, included quantity, and overage price directly. It also shows the
selected plan's entitlement bundle, support, and activities that are included
rather than IU-rated. The package price is the complete subscription economics;
there is no second “platform access” fee. Only one complete Offer is effective
for acceptance at a time.
A later discount or Rate Card change never rewrites an accepted term. Changes
apply through a new offer or successor term that you can review.
## Product access and premium support
When a plan includes product access, the Offer shows an immutable
bundle identity and revision plus the exact feature keys included. Apostra validates
those keys against its canonical feature registry before issuing the offer. An
unregistered feature, an alias in place of its canonical key, or a feature that is
not contract-gated cannot be issued as an authoritative entitlement bundle.
A plan can also include premium support. The Offer preserves one
of two simple forms: unlimited premium support, or a monthly premium package with
included hours. Additional support follows the Enterprise agreement or statement
of work; it is not an automatically metered IU activity. Coverage windows,
severity targets, onboarding allowances, and escalation policies are not separate
commercial fields.
The proposal's **Included** scope and the structured premium-support package are
separate terms. The customer view shows every included-scope line exactly as the
server-authored proposal records it, while it shows a named support package only
when the Offer carries one. A scope line must not be silently converted into a
support package, or vice versa. If its meaning is unclear, the issuer and
customer must resolve it in the audited Offer preview before acceptance; a Task
does not infer a support commitment from ambiguous wording.
Acceptance preserves these exact product and service terms in the acceptance record and
creates each contracted product right against that same record atomically.
Runtime-gated rights become usable only while the accepted terms and governing contract
are in force. The paid right to operate agents for clients is one such runtime right; sales-agent registration access,
registration, deterministic certification, and certification evidence are free and cannot
be sold in an entitlement bundle. Setup rights remain explicit fulfillment obligations;
acceptance does not silently assign a support owner or bypass payment standing for
chargeable operations.
Each plan also declares the default feature profile that presents those rights
as one coherent product experience. The profile is not a second plan or an
independent grant of access: Apostra validates that every entitlement the
profile requires is included and active. When you accept a plan, its
entitlements and default profile are provisioned together; if they are
incompatible, neither change is applied. An approved beta or operator-assisted
profile transition follows the same rule and cannot add a commercial right that
the organization does not already hold. Renewals preserve the accepted
profile revision with the successor term rather than silently adopting a later
profile definition.
## Rollover
On a committed plan, unused included IUs can carry into the next monthly period
up to 50% of that next period's included quantity:
* the 250 plan can carry at most 125 IUs; and
* the 1,000 plan can carry at most 500 IUs.
Carried IUs expire after that one successor period and cannot accumulate again.
Pay as you go provides no monthly allocation to roll over. Setup and
promotional credits never roll over.
## Enhanced Reporting
Connecting and mapping an account costs nothing. The catalog default for
**Enhanced Reporting** is **4 IUs per connected account per billing period**
after its account-level control is enabled and that exact account's existing
reporting subscription completes a successful sync. The control is set for one
exact connected account; it does not enable sibling accounts or change ordinary
connection and campaign features. An enabled account without a reporting
subscription or successful sync draws no IUs. Enhanced Reporting is a required
term on every new Rate Card, and every plan in the current template waives it.
Ordinary connection, synchronization, and campaign actions are included.
This is an activity term, not a buyer plan. The selected plan resolves the
catalog default before acceptance. For example, five eligible accounts would
have a 20-IU catalog total, but the current plans' waiver resolves that total to
zero.
The catalog keeps the 4-IU default visible, while the selected plan records the
waiver. **Usage & credits** applies the resolved terms from your accepted Rate
Card. A future customized Offer would have to show any different activity term
before acceptance. Charging is switched on per organization, never silently,
and accepting a plan does not itself start it—see [Billing cycle and fee
invoices](#billing-cycle-and-fee-invoices).
The 4-IU count is the current standard, not a final one. Your accepted version
is what applies to you: a change reaches you only through a successor Rate
Card that you accept, and it never reprices a period already metered.
## Complete activity schedule
Every Rate Card shows the same complete activity list. Each row starts with the
catalog unit and price. The selected plan then applies its activity terms, and a
Commercial Offer can apply an authorized customer-specific adjustment after
that. Either layer can set a custom IU price or waive the activity; an activity
without an override keeps the preceding value. Acceptance freezes the resolved
result, so later catalog, plan, or Offer changes cannot rewrite it.
The row status is part of the Rate Card. **Published** means an admitted meter
may debit the accepted IU balance. The v4 catalog contains exactly the three
published rows below. The accepted Rate Card freezes its catalog version and
resolved rows, so later catalog changes do not rewrite the agreement. Existing
accepted v0-v3 Rate Cards keep their earlier catalog and terms.
Historical agreements that predate the complete schedule retain only the
activity charges they explicitly contained. Showing the newer catalog does not
add a charge to one of those agreements; an activity absent from that immutable
snapshot stays absent until a successor Offer is accepted.
| Activity | Starting rate | Included boundary |
| - | -: | - |
| Brief response | 1 IU/response | One completed Apostra merchandising cycle |
| Enhanced Reporting | 4 IUs/account billing period | One exact connected account per billing period after its control is enabled and its existing reporting subscription completes a successful sync; ordinary connection, synchronization, and campaign actions are included |
| Apostra media buy | 1 IU/media buy billing period | One qualifying non-social Apostra media buy per billing period with positive impressions or spend |
Apostra media-buy qualification uses the latest authoritative delivery totals for
that buy and billing period. If a correction leaves both impressions and spend at zero
before invoicing, the provisional IU is removed. If the correction arrives after
invoicing, it is handled through the normal auditable one-IU credit or reversal process
against the original period. A later positive correction can reinstate the same unit
once, but cannot create an additional unit for that buy and period.
Customer approval actions, retries, routing, caching, and internal model calls
never create additional charges. Human expert support and Apostra-operated
modular-source fulfillment are contracted managed services, not metered IU
activities. Custom modular-source composition is included with Premium and
Enterprise through the Merchandising profile, not a per-source usage charge or
separate entitlement.
## New Seller Account setup credit
When an effective Rate Card makes the offer available, a seller billing
organization receives one **100-IU credit** on its first IU plan acceptance. You
do not need to be a new customer: if your organization has been with us for a
while and is accepting an IU plan for the first time, the credit is yours too.
The credit is valid for **60 days**, and it can be spent on **any IU-priced
activity** — it is not limited to setup work. The 60 days run from your signup
commit as a fixed window, not a number of billing periods, so it does not move
with your billing cycle. **Usage & credits shows the exact expiry date** rather
than asking you to derive it. Seller Account provisioning may complete
asynchronously without moving that window.
One credit per billing organization: later Seller Accounts do not create additional
credits. It does not roll over, has no cash value, and does not stack. A valid
promotion or referral code can attribute the same credit and may carry an
authorized pricing or access adjustment, but it cannot add a second credit or
change the program's value or expiration. The preview is recomputed after the
code is entered, and the adjusted Effective Rate Card—not the code itself—is the
accepted record.
The setup-credit offer is available only within the controlled staging pilot;
it is not currently available in production and does not create billable usage.
## Acceptance and renewal
When an effective Rate Card is available, **Settings → Plan & Billing** shows:
* the exact Rate Card version and effective date;
* every eligible plan size;
* your complete Effective Rate Card, including any corporate adjustment,
promotion, referral, or explicitly customized package;
* the exact entitlement-bundle revision, included feature keys, and premium-support
package when the offer carries them;
* a frozen customer proposal, when assigned, containing the scope, initial term,
renewal and notice terms, and the exact monthly and/or term-prepayment choices;
* included IUs, rollover, overage, billing timing, and term; and
* the governing agreement and renewal posture.
The online proposal and its downloadable PDF come from the same immutable Offer
snapshot. The PDF is labeled **Draft — not yet accepted** until the Offer has been
accepted. An accepted PDF records the canonical Offer version, acceptance date,
and selected payment choice; alternatives remain visible as not selected. If the
negotiated terms change, Apostra revokes or supersedes the Offer and issues a new
version; it does not edit the version you were sent.
The organization name on the proposal comes from the organization account, not
from a separate sales profile. If it is incorrect, Apostra updates the organization
record and issues a new Offer. Billing contacts, payer name, tax information, and
billing address likewise remain in the organization's Billing information.
When an Offer contains multiple payment choices, the administrator selects one
before accepting. The client submits only that option's key; Apostra resolves and
stores the complete frozen option as immutable acceptance evidence. A full-term
prepayment is one exact price in the Offer currency for the complete service
term; it is not a partial-term payment or a rounded number of monthly payments.
After
acceptance, **Set up payment** opens the organization's payment setup. Automated
term-prepayment collection is not yet self-service, so Apostra coordinates its
invoice or ACH funding separately and does not run monthly base charges for that
schedule.
After acceptance, invited staging organizations may also see the exact included
allocation, remaining balance, overage rate, rollover policy, observed usage,
and projected month-end usage on **Usage & credits**. These staging figures do
not create charges or enforce entitlements.
Only a verified organization administrator can accept a plan. An agent, API
credential, service account, or impersonated support session cannot make that
financial commitment. If the offer changes while you are reviewing it, the
confirmation reloads and asks you to review the new version.
Signup may present the current Terms of Service and IU Rate Card in one flow,
but the acceptances remain distinct: the record preserves the exact agreement,
Rate Card version, selected plan, complete adjustments, prices, actor, and time.
An existing organization without an accepted plan will receive the same **Choose an
IU plan** action after a Rate Card becomes effective. Apostra never silently
accepts a paid plan on its behalf.
The plan task also lets an administrator **Continue without a paid plan** or
**Decide later**. Continuing without a plan dismisses automatic login prompts
for only the exact Rate Card revision reviewed; a successor revision may be
shown again. Deciding later allows Apostra to show the task on the next
login. In either case, **Settings → Plan & Billing** keeps the manual **Choose an
IU plan** action available while the offer remains current, so you can return
without waiting for another prompt.
One acceptance applies to the billing organization. If you accepted from a
Seller Account task, the buyer view shows the same plan; if you accepted from a
buyer task, the Seller Account view does too.
Standard plans and private offers marked **pin exact** renew through immutable
monthly terms at the accepted price. An offer marked **review required** stops
before automatic renewal and requires a new explicit commercial decision. A
later Rate Card or adjustment never silently reprices the current term. Ending
the IU plan at term is an organization-wide decision: it prevents the next
shared plan term from starting, rather than ending only one buyer or Seller Account
capability. Individual capabilities are enabled or disabled separately.
## Billing cycle and fee invoices
Once charging is enabled for your organization (it is switched on per
organization, never silently — accepting a plan does not itself start
charging), your IU plan bills on the **calendar month**:
* **Commitment at cycle start.** The plan's monthly commitment is charged at
the start of each calendar month, at the price fixed in your accepted
terms.
* **Overage during the cycle.** Usage beyond your included and rolled-over
IUs accrues as overage at your plan's per-IU rate. For organizations paying
by card, accrued overage is charged automatically once it crosses a
threshold, so month-end never brings one large surprise; any remainder
settles when the cycle closes.
* **A fee invoice at cycle close.** Every charged month produces a numbered
fee invoice — plan commitment, overage (IUs × rate), and payments applied —
on **Settings → Plan & Billing → Payment & invoices**. Invoice numbers are
sequential with no gaps, and an issued invoice's charges never change;
payments are appended as they settle.
Commitment billing and usage billing are switched on separately. Your
organization can have commitment billing on while usage billing is still off;
the reverse cannot happen. While usage billing is off, overage still accrues
and stays visible on **Usage & credits**, but it is not charged and no overage
line appears on your fee invoice — so a month in which you used more than your
included and rolled-over IUs can still invoice as the commitment alone. Overage
that a closed month did not bill is not collected later: issued invoices never
change. If usage billing is switched on during a period, overage already
accrued in that same open period becomes chargeable.
Organizations paying by card are charged automatically against the saved
card. Invoiced organizations receive the fee invoice only and pay on their
existing terms — no card charge is made.
If a card charge fails, the standard payment recovery ladder applies: retries
on a published schedule, email notices, and a self-serve **pay now** action on
Plan & Billing that clears the failed-payment status once it succeeds. Plan &
Billing also reports unresolved dunning-backed holds, while reaching the
account's cap records an internal hold. In the current alpha, neither hold is
consulted by a production authorization path, so it does not automatically
block new paid platform activity. Viewing, reporting, exporting, and fixing
your payment method remain available.
## For agents and API clients
`GET /api/v2/billing/iu-rate-card` is the canonical organization offer read.
`GET /api/v2/billing/fee-invoices` lists the organization's fee invoices, and
`GET /api/v2/billing/fee-invoices/{invoiceNumber}` returns one invoice with
its line items.
Agents should use the typed `open_iu_plan_task` tool to open the same portable
plan-selection task rather than reconstructing it from a generic API call. Its
model-visible receipt is deliberately bounded; the authenticated app receives
the exact commercial state. `get_iu_rate_card_offer` remains available only as
a compatibility read for deployed clients.
Acceptance and renewal changes are direct human commitments. The task invokes
app-only operations to append the displayed, declined, deferred, or accepted
outcome and to schedule or reinstate renewal. Every response is idempotent and
tied to the exact Rate Card revision; an accepted outcome links to the immutable
acceptance record. These commit operations are not model-visible. The older
`storefront-rate-card` REST paths and storefront-named tools remain deprecated
compatibility aliases; new clients should use the organization IU names.
## Separate from media pricing
IUs price platform activity. Existing media-pricing arrangements cover media
spend and their accepted contract fee terms. This Organization IU Rate Card
does not replace, amend, or silently remove those terms. Plan & Billing can show
both because an organization may owe both platform/IU charges and media charges,
but each is a separate commercial stream.
## Related
Where organization administrators review commercial terms
Payouts, settlement, and the Seller Account view of the shared plan
# Billing
Source: https://docs.apostra.com/v2/buyer/billing/overview
How media billing responsibility and platform-fee collection work, what remittance details to use, and how to keep billing info current
Who invoices media is set per transaction by its **billing party**:
* **Consolidated billing (`agent`)** — the seller sends its invoice to Apostra
instead of to you. Apostra finance puts it on your consolidated media invoice,
which carries the bank remittance details to pay against, and passes your
payment to the seller once it arrives. Apostra acts as a billing agent: it
does not guarantee the seller's payment, does not pay the seller before you
pay, and takes no credit risk on the seller's media. The seller carries that
credit risk.
* **Operator-billed** — the seller or connected platform bills the account
operator directly.
* **Advertiser-billed** — the seller or connected platform bills the advertiser
directly.
For compatibility, `agent` projects to legacy `Decisioned`, while `operator`
and `advertiser` both project to legacy `Routed`. That projection is lossy:
`Routed` cannot distinguish the operator from the advertiser. Billing party
remains the authority for who invoices; `routingType` does not say whether
optimization happened or which adapter/execution path was used.
For a seller-sponsored buyer, the sponsoring seller fixes this authority at
provisioning time: `prepay` selects consolidated billing (`agent`), while `credit`
selects seller direct-billed `operator`. The sponsored account remains the
seller's governed account, and the buyer cannot change that billing choice.
Platform/IU fees are a separate billing stream from media. For organizations
whose IU Rate Card and charging feature are enabled, Apostra issues a separate
fee invoice and may collect it from the card saved on Plan & Billing. Existing
accepted media-pricing arrangements remain separate and unchanged.
Sponsored `prepay` uses the existing media invoice/deposit funding and
account-capacity mechanisms. It does not make platform-fee card collection an
automated media-funding flow. Sponsored `credit` is seller direct billing,
not an Apostra-granted credit line.
Keep your **billing info** current for Apostra-issued media or platform-fee
invoices — who the invoice is addressed to and where it is sent. Manage it in
**Account settings → Plan & Billing → Payment & invoices** or via
the API (updates are admin-only):
```
GET /api/v2/billing/info
PUT /api/v2/billing/info
```
Authenticate every request with `Authorization: Bearer $SCOPE3_API_KEY`.
## What Apostra needs for its invoices
The minimum for an invoice you can pay — and that your accounts-payable team
will accept — is a **payer name** and a **payer address**. Beyond that, keep
these current so invoices route correctly:
| Field | Purpose |
| - | - |
| Billing contact name | Who the invoice is addressed to |
| Billing email + CC emails | Where the invoice is delivered |
| Phone | Finance-team contact for payment queries |
| Billing address | The payer address printed on the invoice |
| Tax ID | VAT/EIN or equivalent for your jurisdiction |
## Where the remittance details come from
Bank remittance details are printed **on each Apostra-issued invoice** — you do
not need to request them separately, and you should always pay against the
details on the invoice you received. Apostra maintains a bank account per
invoicing currency, so the remittance details match the currency you are billed
in. A card-collected platform/IU fee instead follows the saved-card flow shown
in Plan & Billing.
## When invoices arrive
Apostra-issued invoices follow your billing cycle and payment terms. A media
invoice covers only consolidated-billing activity; operator- and
advertiser-billed media follows that counterparty's invoice process. Questions
about an Apostra invoice go to the finance contact listed on it.
## Billing activity
The **Invoice activity** section at the top of the Invoices tab on the org
Billing page shows invoice activity as it happens — status, what it's for,
currency, amount, and due/paid date — via
[Get invoice activity](/v2/buyer/billing/tasks/get-invoice-activity). It's
empty until your organization has an issued IU fee invoice; media invoice
activity and other reserved row kinds do not have producers yet.
## Task reference
`GET /billing/info` — current contact, address, tax ID
`PUT /billing/info` — keep invoicing details current
`GET /billing/invoice-activity` — invoice activity as it happens
# Plan & Billing page
Source: https://docs.apostra.com/v2/buyer/billing/plan-and-billing
One page for your commercial boundary — plan, balance, usage, agreements, and the next step to stay in good standing
Plan & Billing is the Account settings section for your account or
organization's commercial boundary. Open **Account settings → Plan & Billing**;
the section stays in the Account settings frame while you move between its
commercial tabs. Older billing and organization-settings links open this same
section. It answers, in one
place: which plan and terms
that boundary is on, what each action costs, what has been used, what is
funded and what is owed, and which agreements are in force — plus the one
concrete step, if any, needed to complete billing setup or stay in good standing.
Selecting **Plan & Billing** opens **Overview** for parent and standalone buyer
or Seller Account organizations. From a child account, Plan & Billing points to the
parent billing organization and opens its Overview; users without parent-admin
access do not receive a billing entry point. Other commercial-boundary links
for a specific task, such as reviewing invoices, open the relevant tab directly.
A standalone Buyer or Seller account is its own commercial boundary until an
administrator explicitly attaches it to an organization. Matching email
domains or CRM classification never move billing, Terms of Service, standing,
or administrators. After attachment, those responsibilities resolve to the
parent organization.
The page's core commercial-boundary facts come from the
[Get billing account](/v2/buyer/billing/tasks/get-billing-account) API. For an
eligible commercial boundary, the Plan & pricing tab adds
the one organization IU plan, complete Effective Rate Card, and renewal state from the
canonical `GET /api/v2/billing/iu-rate-card` read. The older
`GET /api/v2/billing/storefront-rate-card` path is a deprecated compatibility
alias; it does not make the plan Seller Account-only. Every billing read uses the
same organization and admin boundary.
The standard [Organization IU Rate
Card](/v2/buyer/billing/organization-iu-rate-card) is published in staging for
a controlled pilot and visible only to invited organizations. It remains
unpublished in production until the pricing and governing terms are approved,
so the production page will not ask you to accept it. Invited staging
organizations can review and accept their exact terms. Enrolled organizations
may also see their accepted included-IU allocation, observed usage, and a
display-only forecast. Existing accepted media-pricing arrangements are
unchanged.
## When charging starts
Charging is available from 22 September 2026. Your billing starts on the
effective date of the plan you accept, so accepting a plan is what begins it
rather than the date itself.
Three things are worth knowing before you accept:
* **The platform-access fee and the subscription fee are one recurring charge**,
not two separate fees.
* **One organization has one plan and one IU wallet**, covering both buying and
selling work rather than a separate plan for each.
* **Accepting a plan locks its terms for that term.** The monthly price,
included IUs, overage price, rollover rule, priced activities, and included
product access are recorded as you accepted them, and a later price change
does not rewrite an accepted plan.
A saved card covers eligible subscription and IU charges. It is not used to fund
media.
Invoice history and downloadable invoice documents are not in Plan & Billing
yet, for subscription or IU charges. Media billing stays outside the platform
as well: media invoices, payment status, disputes and credit notes, and seller
payouts all continue through the existing process rather than through Plan &
Billing. We are holding the invoice experience back until it can show you the
complete billing picture rather than part of it.
## Who can see it
Plan & Billing describes money for the current **commercial boundary**, so it
follows that boundary's admin authority:
* **Admins of an organization or standalone account** see the full
page for the boundary they administer.
* **Admins of a child account who also administer its parent organization** are
forwarded to the parent's page — billing, including authority over payout
destinations, lives at the organization level. A child-only admin cannot
open or change the parent's billing information.
* **Ordinary members** don't see the page or its Settings entry at all.
You do not need any internal or Apostra-side role to view your own effective
commercial terms — admin standing on the relevant boundary is enough.
## Key figures
**Available balance** appears only for organizations that prepay and hold a
funded balance. It is your funded balance minus any amount owed. Apostra does
not grant buyer credit lines, so it never includes a credit limit. If your
organization pays on terms, Plan & Billing shows what you owe and your
invoices instead of an available balance.
**Intelligence usage this period** is the customer Intelligence Unit (IU) total
for the current billing period. It matches the priced IU activity shown in
**Usage & credits** and does not include internal AI-provider operating costs.
## The next-action banner
The banner at the top of the page shows the **single most important thing to
do next**. On Overview, a four-step setup indicator puts that action in context:
accept the current terms, select an IU plan when the organization is eligible,
add billing details, then add a payment method. Each marker reflects the
commercial boundary's current facts, so a later step completed early stays
complete if an earlier prerequisite reopens. Organizations that are not eligible
for an IU plan continue directly to billing details. Established boundaries also
receive funding and standing-recovery actions when needed:
| Next action | What it means |
| - | - |
| Accept terms | The current Terms of Service haven't been accepted yet. |
| Accept a governing agreement | A private IU plan offer is ready, and its governing agreement must be reviewed before plan selection. |
| Choose an IU plan | An effective Organization IU Rate Card is available for review. When billing is enabled, this commercial boundary must choose a plan before it can use IU-priced capabilities. |
| Add billing info | No billing contact or payer address is on file, so Apostra can't invoice you. |
| Add a card | This commercial boundary has no payment method yet — [saving a card](/v2/buyer/billing/concepts/pay-by-card) makes it available for eligible platform/IU charges. Saving a card does not accept an IU Rate Card, select a plan, or activate paid entitlements. |
| Fund your account | This commercial boundary is on prepay and its balance is exhausted. |
| Resolve a hold | A card payment remains unresolved. The hold is a billing-status signal; the current alpha does not enforce it as a spend or entitlement gate. |
| None | Nothing outstanding — the account is in good standing. |
Only the highest-priority action is returned to the page and compact Plan &
Billing app. When there's nothing to do, the banner says so. When there is, it
links to the tab or plan-review task that resolves it.
## Choose how to pay
On **Payment & invoices**, a business without a payment method can
[save a card](/v2/buyer/billing/concepts/pay-by-card) to pay Apostra's
platform/IU fees by card, with no commitment. Plan & Billing no longer offers
a credit application. Buying from sellers on consolidated billing needs only
your billing details (payer name, street address, and country); it does not
require a card or a credit line.
If a seller separately extends payment terms for the media it sells you,
that arrangement is between you and the seller — sellers decide credit on
their own terms, not Apostra.
For a business with one buyer account, this appears as one business billing
setup; you do not need to manage an organization/account hierarchy. The
business remains the owner of the card and billing history. Buyer accounts
separately own operator identity, advertisers, and campaigns, so adding
accounts later does not move or recreate the business's payment authority.
During the staging pilot, eligible administrators at invited organizations may
see an automatic login prompt with **Choose an IU plan**, **Continue without a
paid plan**, and **Decide later**. Continuing without a plan suppresses that
prompt only for the exact Rate Card revision; deciding later permits it again on
the next login. The manual **Choose an IU plan** action on Plan & pricing remains
available in both cases while the organization is eligible and has not accepted
a plan.
## What each tab shows
| Tab | Content |
| - | - |
| **Overview** | Account standing (billing method: prepay or credit, effective payment terms), available balance with funded/owed breakdown (prepaying organizations with a funded balance only), intelligence usage for the period, and — for organizations with a Seller Account — a shortcut to the Payouts tab. |
| **Plan & pricing** | Your effective terms: the exact Rate Card revision and complete selected plan, including pricing and IUs, rollover, the immutable entitlement-bundle revision and feature keys, any premium-support package, proposal terms, included activity terms, effective and renewal dates, and whether the Offer uses standard plan terms or explicit customizations. A customized Offer records its reason and exact differences from the source plan. A private proposal also shows its executive summary, included scope, initial term, automatic-renewal and non-renewal notice terms, and every exact payment choice. You can download the same frozen proposal as a PDF for internal approval. The package price is the complete subscription price; there is no second platform-access fee. The **How you're charged** card keeps IU/plan charges separate from existing accepted media-pricing arrangements. When a plan has been accepted, this tab also shows automatic-renewal status, the change cutoff, and any scheduled no-plan date. |
| **Usage & credits** | When the IU account experience is enabled: IUs consumed by each ungrouped, priced activity and by day; the exact accepted included-IU allocation, remaining balance, overage rate, rollover policy, and display-only month-end forecast. If no priced activity consumed IUs, the page says **No IU activity this period**. AI-provider operating cost does not count as customer IU usage. The tab also shows your funded, owed, and available balance. |
| **Payment & invoices** | Two labeled money streams. **Apostra fees**: the business's billing details, payment method, account standing, funding, and platform/IU fees. **Media billing**: activity for invoiced media spend. The two streams stay separate. |
| **Media billing** | For organizations with buyer capability: configure [media billing entities](/v2/buyer/billing/media-billing), attach advertisers/accounts, and preview the most-specific match in the “Which configured entity matches?” inspector. This is currently configuration and preview only; the invoice producer does not yet consume the resolution. |
| **Payouts** | For organizations with a Seller Account: payout entities (one per legal entity and currency, with a primary), setup wizard, and payout activity — including setup history showing who changed payout details and when. Parent admins select the organization or a direct child Seller Account from the account dropdown; they also see per-account payout status. |
| **Contracts & orders** | Contract lifecycle (status, terms-of-service version, effective and end dates), agreement documents with who accepted them and when, the organization IU plan history when published, and the separate media rate-card detail. |
| **Accounts** | For parent organizations only: the money view of your child accounts — customer IU usage attributed to each account during the organization's current billing period. A child with accepted rating terms and no activity shows 0 IUs; unavailable or unratified usage stays blank. Account-level fee totals and billing delegation controls remain hidden until their product contracts are ready. Switching into an account stays in the account switcher; this tab is about money, not navigation. |
For an organization that offers a sales agent to other sellers, a private Offer may include the paid
`operate-agents-for-clients` feature key and its support or IU allowance.
Sales agent registration, testing, and certification remain free and do
not appear as purchased features. This entitlement also does not buy a
buyer-visible badge, organic rank, or Murph recommendation preference; Certified
Partner verification and any labeled Featured Partner placement are separate
program decisions. See [Partner Program and agent
operations](/v2/storefront/inventory-sources/partner-account).
## Private offer signup links
Apostra may send one named person a private signup link for a reviewed private Offer.
**Who can claim it:** only the person the link was sent to. They must be signed in with that exact email address and be an administrator of their organization's billing account.
**What claiming does:** it connects your organization to the Offer and makes that exact Offer available here for review. The Offer carries the same proposal, terms and price that were reviewed before the link was sent.
**What it does not do:** claiming never accepts the Offer, starts paid service or charges a card. You still review and accept the Offer, and choose how to pay, as described above.
**Limits:**
* A link can be claimed once.
* It expires after the period set when it was sent, at most 30 days.
* Apostra can revoke a link or replace it with a newer one.
* If the published terms behind the Offer change after the link was sent, the old link stops working and a new one is needed.
**When a claim cannot finish:** you see the reason, for example an expired or already-used link, a different signed-in email, or a missing billing-administrator role. Sign in with the invited email, ask an administrator to claim it, or ask the person who sent the link for a new one. Some cases, such as an organization already connected to a different Apostra account, are recorded for the Apostra team to review, and you don't need to retry. If we could not record the claim for review, you are asked to try again.
## Media billing responsibility
The transaction's **billing party** is the authority for who invoices and
settles media:
* **Agent** — consolidated billing: the seller's invoice comes to Apostra,
Apostra finance bills you on your consolidated media bill and collects, and
passes your payment to the seller once it arrives. Apostra does not
guarantee payment to the seller or take credit risk on the seller's media.
* **Operator** — the seller or connected platform bills the account operator
directly.
* **Advertiser** — the seller or connected platform bills the advertiser
directly.
For compatibility, `agent` projects to legacy **Decisioned**, while `operator`
and `advertiser` both project to legacy **Routed**. That projection is lossy:
**Routed** cannot distinguish operator from advertiser. Billing party remains
the authority for who invoices; `routingType` does not say whether Apostra
optimized the transaction or which adapter/execution path was used. See the
[campaign object](/v2/object-guides/campaign) for the canonical field
relationship.
## Organization IU plan renewal
After an IU Rate Card is published and your commercial boundary has accepted a
plan, an administrator for that boundary can select **Manage renewal** from Plan & pricing,
then choose **End plan at term** before the displayed change cutoff. The current
plan and price stay in force through the term end; the page shows a pending
non-renewal and the exact timestamp the commercial boundary will return to its
no-paid-IU-plan posture.
This is an organization-wide decision because buyer, Seller Account, Murph, and
other IU-rated workloads use the same plan. It does not terminate the platform
agreement or change an existing accepted media-pricing arrangement. Individual capabilities can be
disabled separately without ending the shared plan.
Before that same cutoff, **Reinstate renewal** restores automatic renewal. Apostra
retains every decision immutably; the renewal task shows the 500 most recent and
labels each entry **Renewal target** or **Earlier term**, so an earlier decision
cannot look like it controls the term being managed. It never edits the accepted
term, and an earlier term cannot be changed after its successor has been created.
Plan & Billing is the one human-facing home for billing: the organization
settings entry redirects here, and the previous contract summary in account
settings also redirects here, so one page answers every billing question.
Some sections show a "coming soon" or non-billing state. **Published price
list** contains only immutable activity terms in the commercial boundary's accepted
Rate Card. **Activities under calibration** shows separately labeled starting
hypotheses for work whose cost and commercial terms are still being measured;
those rows cannot debit the IU balance or alter an accepted contract.
**Deposits history** lists your recent prepay deposits, straight from the
platform ledger. Budgets, auto-top-up, and invoices populate as their
underlying commercial capabilities launch.
## Money figures
Every money figure on the page renders from the ledger — balances, owed
amounts, and payment state are never inferred or cached. The figures match
what [Get billing account](/v2/buyer/billing/tasks/get-billing-account)
returns, in the commercial boundary's billing currency.
## For agents
The core account document is available to agents via
`GET /api/v2/billing/account` — see [Get billing
account](/v2/buyer/billing/tasks/get-billing-account) for its full contract,
including service-token access and redaction. Organization IU plan and renewal
facts come from `GET /api/v2/billing/iu-rate-card`. Agents should use the typed
`open_iu_plan_task` tool to open and present the shared portable task.
Showing, declining, deferring, or accepting the offer appends an idempotent
response tied to the exact revision. Accepting a plan or changing renewal
requires an organization administrator's explicit confirmation; only the task
may invoke those app-only commits, which are not model-visible. The
`storefront`-named REST path and tools remain deprecated compatibility aliases.
## Related
Shared plans, rollover, activity terms, and availability
The consolidated document behind this page
How invoicing and remittance work
# Campaign autonomy settings
Source: https://docs.apostra.com/v2/buyer/campaigns/autonomy-settings
Control whether the agent acts automatically or pauses for buyer approval per campaign
Autonomy settings let a buyer control how independently the agent operates on a campaign. The preference persists across sessions, so the buyer does not have to repeat it each time they start a conversation.
## Two dimensions
Each campaign has two independent autonomy dimensions:
| Dimension | Controls |
| - | - |
| `inventorySelection` | Whether the agent stages media buys automatically or pauses for buyer approval before each one |
| `rebriefing` | Whether the agent updates the campaign brief automatically or proposes changes for buyer approval |
## Three modes
Each dimension accepts one of three mode values:
| Mode | Meaning |
| - | - |
| `manual` | Human-driven. The agent proposes the action and the buyer executes it. This is the default when no preference has been saved. |
| `propose` | Agent drafts, buyer approves. The agent prepares the action and presents it for an explicit yes before proceeding. |
| `automatic` | Agent acts. The agent executes the action without a per-action confirmation, relying on the campaign brief and budget as the standing authority. |
## Setting autonomy via the API
Pass the `autonomy` field to the campaign create or update operation. Each dimension is optional — omitting a dimension leaves its current value unchanged.
```json theme={null}
{
"autonomy": {
"inventorySelection": "automatic",
"rebriefing": "propose"
}
}
```
At least one dimension must be present when the `autonomy` object is supplied.
## Reading the stored preference
The response shape differs between the V2 REST API and the V3 MCP agent surface.
### V2 REST API
The V2 REST campaign response includes `autonomy` only when at least one dimension has been explicitly set. Each present dimension is returned as a plain enum string:
```json theme={null}
{
"autonomy": {
"inventorySelection": "automatic"
}
}
```
An absent dimension means no preference has been saved and the effective mode is `manual`. The `autonomy` object itself is absent from the response when neither dimension is set.
### V3 MCP agent surface
The V3 campaign workspace always returns both dimensions, each as an object:
| Field | Values | Meaning |
| - | - | - |
| `mode` | `manual` \| `propose` \| `automatic` | The effective mode |
| `source` | `inherited` \| `overridden` | How the mode was determined |
A `source` of `inherited` means no preference has been saved and the dimension is using its default (`manual`). A `source` of `overridden` means the buyer explicitly set the mode.
```json theme={null}
{
"autonomy": {
"inventorySelection": { "mode": "automatic", "source": "overridden" },
"rebriefing": { "mode": "manual", "source": "inherited" }
}
}
```
## Scope and persistence
Autonomy is a per-campaign setting. Changing it on one campaign does not affect other campaigns for the same advertiser or account. The preference survives session restarts and is included in every campaign read response.
# Campaign suggestion sets
Source: https://docs.apostra.com/v2/buyer/campaigns/campaign-suggestion-sets
How optimizer suggestions that move budget between media buys are approved
When Apostra's optimizer works across a whole **campaign** rather than one media buy at
a time, its recommendations stop being independent. Taking budget out of one
media buy is what funds putting budget into another, so the two halves are one
decision. A **campaign suggestion set** is that decision: every media buy the
optimizer moved money between, grouped under the single run that computed them.
If you are not receiving sets yet, nothing is missing — see
[Availability](#availability).
## Why a set is approved as a whole
Each member of a set carries a `netAllocationDelta` — the amount that media buy
gains or gives up. Those are transfers, not standalone recommendations, and they
sum to exactly zero across the set.
That is why there is no way to approve one media buy's share on its own.
Approving a `+4000` minor units without its offsetting `-4000` would create
budget that the campaign never freed; approving the `-4000` alone would strand it. And rescaling a
subset so it balances by itself produces an allocation the optimizer never
evaluated against your goals or your sellers' capacity. So the set is the unit:
you approve all of it, or none of it.
The per-media-buy breakdown is there so you can see what the transfer does —
[read a single set](/v2/buyer/campaigns/tasks/get-campaign-suggestion-set) to get it.
It is not there so that parts of it can be actioned separately.
## The lifecycle of a set
The optimizer publishes one message per media buy it evaluated, and they
arrive independently. Until all of them have, the set is incomplete and
Apostra will not act on it — applying half a transfer would leave your
campaign's budget unbalanced. A set can sit here legitimately while the rest of
the run arrives.
The set is complete and balanced, and it is yours to decide. You are notified
when a set reaches this state.
Your decision applies to the whole set.
[Approve](/v2/buyer/campaigns/tasks/approve-campaign-suggestion-set) it, or
[reject](/v2/buyer/campaigns/tasks/reject-campaign-suggestion-set) it — rejection
optionally carries a reason; approval takes no body.
Apostra sends the changes to your sellers and waits for each one to take
effect. This is not instant — see [What happens after you
approve](#what-happens-after-you-approve).
If optimizations are set to apply automatically, a complete set goes straight to
approved without waiting for you. That setting is read from the campaign, falling
back to the advertiser's default when the campaign does not set one — so a set
can auto-approve on a campaign that appears to specify nothing. It is fixed when
the run is first seen, so changing it mid-run affects the next run, not one
already in flight.
## What happens after you approve
Approving does not move the money. It clears Apostra to move it, and the
move takes time, because each media buy lives at a different seller and a seller
takes a change when it takes it.
Two things about that are worth understanding, because they are what you will
see while polling.
**Budget is freed before it is spent.** Every media buy giving budget up is
changed first, and Apostra waits until each seller has actually enacted its
decrease before sending a single increase. It never commits budget the campaign has
not yet freed. That waiting is the reason a set stays in `APPLYING` for a while,
and it happens twice — once for the decreases and again for the increases. How
long that takes is up to your sellers: often minutes, longer when one queues the
change for review. Apostra keeps checking rather than assuming, and if a
seller goes quiet it stops and raises the set for a person rather than waiting
indefinitely.
**If a change cannot be made, the whole transfer is put back.** A set is one
decision, so it is applied as one. If a media buy's change fails after another
buy's change has already landed at its seller, Apostra restores every media
buy it had already moved to the budget it held before the set was applied.
The one exception is a reversal that cannot itself complete — a seller refuses
the undo, or never enacts it. Apostra then stops rather than guessing, moves
the set to `NEEDS_RECONCILIATION`, and a person puts the budgets right. Until
they do, your live budgets can sit part-way between the suggestion and their
pre-approval values; see the statuses below.
A media buy whose **flight has ended** while the set was applying is left out —
of the transfer, and of the reversal too. A finished buy cannot spend what a
budget change would give it, and failing the set over one would undo the other
buys' correct changes over a calendar date. The set still completes and names
the media buys it left out, on `statusReason`.
The exception is a media buy that was **giving budget up**. If its flight ends
before its seller has enacted the decrease, that budget was never freed — the
buy may already have spent it — so Apostra does not send the increases it
would have funded. The whole transfer is put back instead, and a later run
re-plans the campaign without the finished buy.
When a finished buy is left out, your budgets will not match either the
suggestion or their pre-approval values exactly. A buy left out of a reversal
may still be sitting at the applied budget, and because its flight has ended,
nothing further will change it.
### Statuses you may see
| Status | Meaning |
| - | - |
| `AWAITING_MESSAGES` | Not every media buy in the run has reported yet. Nothing to decide. |
| `AWAITING_APPROVAL` | Complete and balanced. Waiting on your decision. |
| `APPROVED` | Accepted — by you, or automatically if the campaign applies optimizations without review. |
| `APPLYING` | In progress: changes are being sent to your sellers and confirmed. How long it lasts is up to your sellers — the decreases are confirmed before any increase is sent. |
| `APPLIED` | Done. Every media buy took its change, except any whose flight had ended — those are named on the set. |
| `COMPENSATING` | Something could not be applied — or a media buy giving budget up finished its flight before its decrease landed — so the media buys that had already moved are being restored to their pre-approval budgets. |
| `COMPENSATED` | The transfer was reversed. Every media buy it could reach is back at its pre-approval budget; any it could not are named on the set. |
| `APPLY_FAILED` | Refused before any change reached a seller. Nothing moved, and there was nothing to put back. |
| `NEEDS_RECONCILIATION` | A seller accepted a change but never enacted it within the hour, or the reversal itself could not complete. Apostra stops rather than guessing. Apostra is alerted and a person is resolving it. |
| `REJECTED` | Declined. Nothing moves. |
| `SUPERSEDED` | A newer, complete run for the same campaign replaced this one. Decide the newer set instead. |
| `EXPIRED` | The recommendation no longer describes your campaign — a media buy in it changed after the optimizer read it, or one reached a terminal state, or the campaign is gone. A later run will produce a fresh set. |
| `PARKED` | Held back because the run did not satisfy its own consistency rules. Apostra is alerted; you do not need to act. |
| `DRY_RUN_RECORDED` | Applying campaign-scoped optimizations is not switched on for your account, so nothing was sent to your sellers. What would have moved is recorded. Which set lands here is decided by the campaign: one you review terminates here instead of being offered to you, and one that applies optimizations without review terminates here instead of applying. Either way it is final rather than queued — nothing retries it, and a later run produces a fresh set. |
Only `AWAITING_APPROVAL` is actionable by you. Everything else is either in
progress or terminal.
`NEEDS_RECONCILIATION` is the one status where your live budgets may not match
either the suggestion or their pre-approval values. Apostra is alerted
automatically and resolves it; you do not need to open a ticket. Do not
hand-edit the affected media buys' budgets while a set is in this state —
doing so changes what Apostra is working from to put them right.
The `status` filter accepts the full enum. Treat an unrecognised status as "not
actionable by me" rather than assuming this list is closed.
## Finding sets that need you
[List campaign suggestion sets](/v2/buyer/campaigns/tasks/list-campaign-suggestion-sets)
and filter on `status=AWAITING_APPROVAL` to see just the decisions outstanding.
You can also filter by `campaignId`.
The list gives you each set's `campaignMediaBuyCount` but not its per-media-buy
breakdown — a single campaign can span many media buys, and a list is for finding the
set you care about. Fetch that one set to see where the money goes.
`campaignMediaBuyCount` is the number of media buys the optimizer **declared** for
the run, not the number reported so far. A set still in `AWAITING_MESSAGES` can
declare four and return an empty `mediaBuys` array, because membership is fixed
only once the set is complete.
## How a set relates to a suggestion
A campaign suggestion set is made of ordinary optimization suggestions — one per
member media buy — and each still appears through
`GET /api/v2/buyer/optimization-suggestions` as usual. What changes is where the
decision lives: a suggestion that belongs to a set cannot be approved or rejected
on its own, and attempting it returns `409` naming the set to decide instead.
## Availability
Campaign-scoped optimization is being rolled out. No campaign-scoped runs are
published yet, so these endpoints return nothing for anyone — they filter
nothing by account, and an empty list today is not a statement about your
rollout. Once runs are published, an account that is not yet enabled will have
its sets recorded rather than acted on, whether or not the campaign applies
optimizations without review, and no approval notification arrives for the ones
you would otherwise have reviewed. Nothing about your existing per-media-buy
optimization suggestions changes in the meantime.
# Channel groups
Source: https://docs.apostra.com/v2/buyer/campaigns/channel-groups
Express composite inventory intent, compile it into media buys, and preserve the same grouping in reporting
A **channel group** is one coherent inventory selection inside a campaign. It
combines AdCP inventory dimensions without inventing new AdCP channel values.
For example, mobile web display means:
```json theme={null}
{
"channels": ["display"],
"propertyTypes": ["website"],
"deviceTypes": ["mobile"]
}
```
It does not mean `channel: "mobile_web"`. `mobile_web` is not an AdCP media
channel.
Interchange uses the channel-group ID to split and label media buys, and a V3
`request_proposals` round uses the groups to choose which sellers to ask and
which returned products to keep (see
[Proposal rounds follow your channel groups](#proposal-rounds-follow-your-channel-groups)).
It does not validate products you select against the group's inventory
definition, or translate that definition into package targeting. Murph and API
clients must verify discovery evidence and seller product details before
assigning `channelGroupId`.
Within one inventory dimension, values are alternatives (OR). Across populated
dimensions, declared values combine (AND). A group with `channels: ["ctv",
"olv"]` and `deviceTypes: ["ctv", "mobile"]` therefore allows either channel
and either device, while still declaring both a listed channel and a listed
device. If you need pairwise logic such as “CTV on CTV devices OR OLV on mobile
devices”, make two channel groups.
## Why groups create media-buy boundaries
One downstream AdCP media buy cannot preserve arbitrary Boolean combinations
across channel, property type, device type, and format. Interchange therefore
uses this compilation rule:
```text theme={null}
channel group × seller × settlement currency = one media buy
```
If a campaign contains `mobile-web-display` and `ctv`, products assigned to
those groups become separate media buys even when they use the same seller and
currency. They remain part of one campaign.
## Save pre-built groups
Pass `channelGroups` to `save_campaign` on `/mcp/v3`. A preset is authoring
shorthand. Interchange expands and saves the full definition with the preset
version, so later preset changes do not silently change an existing campaign.
```json theme={null}
{
"advertiserId": "123",
"name": "Streaming launch",
"flight": {
"startAt": "2027-02-01T00:00:00Z",
"endAt": "2027-02-28T23:59:59Z"
},
"budget": { "total": 50000, "currency": "USD" },
"channelGroups": [
{
"channelGroupId": "mobile-web-display",
"presetId": "mobile_web_display"
},
{
"channelGroupId": "ctv",
"presetId": "ctv"
}
],
"idempotencyKey": "streaming-launch-2027-02"
}
```
The available presets expand as follows:
| Preset | Saved inventory definition |
| - | - |
| `display` | `channels: ["display"]` |
| `olv` | `channels: ["olv"]` |
| `ctv` | `channels: ["ctv"]` |
| `mobile_web_display` | `channels: ["display"]`, `propertyTypes: ["website"]`, `deviceTypes: ["mobile"]`, and AdCP display `formatKinds` (`image`, `html5`, `display_tag`, `image_carousel`, `native_in_feed`, `sponsored_placement`, `responsive_creative`, `custom`) |
| `mobile_web_olv` | `channels: ["olv"]`, `propertyTypes: ["website"]`, `deviceTypes: ["mobile"]` |
The simple channel presets deliberately make no property, device, or format
assumption. For example, `ctv` means the AdCP CTV channel; it does not silently
mean only a physical television device or only a CTV app.
## Define your own group
You can provide the same AdCP-aligned dimensions directly:
```json theme={null}
{
"channelGroupId": "premium-streaming-video",
"name": "Premium streaming video",
"inventory": {
"channels": ["ctv", "olv"],
"propertyTypes": ["ctv_app", "website"],
"deviceTypes": ["ctv", "desktop"],
"formatKinds": ["video_hosted", "video_vast"]
}
}
```
Use separate groups when combinations must not collapse into a cross-product.
Do not put an alias such as `mobile_web` into `channels`; aliases belong in the
group name or preset ID, while the saved inventory definition stays aligned to
AdCP.
## Proposal rounds follow your channel groups
When a campaign has channel groups, a V3 `request_proposals` round uses them in
three ways:
1. **Which sellers are asked.** A seller is contacted only when at least one of
the channels it declares can match a channel in one of your groups. Channel
aliases match their AdCP value: a seller listed as `audio` matches
`streaming_audio`. For an official platform integration, such as Spotify or
Amazon, the platform's own channel coverage is used. A seller that declares
no channels is still asked, because nothing shows it cannot match.
2. **What each seller is told.** Every contacted seller receives the union of
your groups' channels as the AdCP `channels` filter on its request. If a
group names no channel, no channel filter is sent, because that group
accepts every channel.
3. **Which products come back.** A returned product whose channels, or whose
creative format kinds, fit none of your groups is dropped, together with any
proposal that allocates budget to it. The seller's outcome reports the
number dropped as `channelExcludedProductCount`. A product that does not
declare a channel or format is kept.
The response lists the eligible sellers that were not contacted in
`skippedSellers`: each entry names the seller, the channels it declares, and
the reason `channel_mismatch`. At most 25 sellers are listed; `total` counts
them all. If no eligible seller can match any group, the round ends `failed`
with `cohortError.code` `NO_ELIGIBLE_SELLERS` and the same `skippedSellers`
list. To include more sellers, add a group for their channel and start a new
round with the new campaign revision.
Seller narrowing needs every group to name at least one channel. If one group
bounds only property type, device type, or format, that group admits sellers of
any channel, so the round asks every eligible seller and sends no channel
filter. Product filtering still
applies the channel and format bounds of the groups that have them. A group
that bounds only property type or device type turns filtering off, because
neither sellers nor returned products declare those dimensions reliably.
A round whose one seller is fixed by the buyer's account, such as a sponsored
buyer or an own-supply sandbox campaign, never lists that seller in
`skippedSellers`. The seller still receives the `channels` filter, and its
returned products are still filtered.
## Assign products when creating a media buy
Discover products, verify that the seller evidence matches one group, then pass
that group's ID when creating the draft. With `save_media_buy`,
`channelGroupId` applies to all products in that creation call:
```json theme={null}
{
"campaignId": "cmp_123",
"sellerId": "42",
"channelGroupId": "mobile-web-display",
"products": [
{
"productId": "sf1:42:bW9iaWxlLWRpc3BsYXk",
"budget": 15000
}
],
"idempotencyKey": "pq_123"
}
```
When using the batch `create_media_buys` operation directly, put
`channelGroupId` on each product or proposal selection. Every selection needs a
group ID when its campaign defines channel groups. A media buy records the
group ID and name that created it. Define groups before the campaign creates
its first ungrouped media buy. Once a saved selection or media buy references a
group, keep it immutable; add a new group instead of removing or redefining it.
The batch operation also accepts fully expanded `channelGroups` in
`campaign.create`. Preset shorthand is available through V3 `save_campaign`;
REST clients either send the canonical saved shape inline or create the
campaign first and pass its `campaignId`.
REST does not accept preset shorthand. Send `name`, `source`, and the expanded
inventory snapshot:
```json theme={null}
{
"channelGroups": [
{
"channelGroupId": "mobile-web-display",
"name": "Mobile web display",
"source": {
"kind": "preset",
"presetId": "mobile_web_display",
"presetVersion": 1
},
"inventory": {
"channels": ["display"],
"propertyTypes": ["website"],
"deviceTypes": ["mobile"],
"formatKinds": [
"image",
"html5",
"display_tag",
"image_carousel",
"native_in_feed",
"sponsored_placement",
"responsive_creative",
"custom"
]
}
}
]
}
```
Always pass product and proposal IDs exactly as discovery returned them. The
qualified `sf1:` value in the example is illustrative; do not construct or
decode it in a client.
## Report by the same group
Use the V3 `get_delivery` tool with the `campaign_delivery` report and the
`channel_group` dimension:
```json theme={null}
{
"report": "campaign_delivery",
"metrics": ["impressions", "spend", "clicks"],
"dimensions": ["channel_group"],
"range": {
"startDate": "2026-09-01",
"endDate": "2026-09-30"
},
"filters": {
"campaignId": "cmp_123"
}
}
```
To read one group only, add `filters.channelGroupId`. Ungrouped legacy media
buys roll up under a null channel group rather than being guessed from their
names, products, or delivery.
Channel groups answer “which coherent inventory selection did the buyer ask
for?” Campaign targeting answers “who, where, or when should it reach?” Keep
those concepts separate even when both mention a dimension such as device.
# Campaigns on connected provider accounts
Source: https://docs.apostra.com/v2/buyer/campaigns/directed-campaigns
Mirror and inspect campaigns from a connected provider account
Provider-account campaign mirroring is available to buyers with an eligible
connected account. It is read-only: Apostra does not create,
update, pause, reactivate, or cancel campaigns in the connected account.
## Tracked campaigns
Connecting a provider account lets Apostra materialize its campaigns as
read-only, `management: "tracked"` projections. The connected provider remains
the source of truth. Apostra periodically reconciles the
projection and provides shared identity, audit, relationship rollups, and live
delivery access around it.
The compatibility API value for these projections is `mode: "directed"`.
`directed` is a deprecated wire discriminator, not a product name. Use the
`management` field to understand who operates a campaign:
* `management: "tracked"` means it was mirrored from provider-account
activity that Apostra did not author. The projection is read-only.
* `management: "managed"` means the campaign uses the canonical Apostra
buyer lifecycle. Discover products and create, read, update, pause, or cancel
its media buys through the buyer media-buy APIs.
Each tracked campaign is a one-to-one projection of one storefront campaign
and one upstream AdCP media buy. Its name, status, flight, budget, and packages
come from that buy and cannot be edited independently. The projection preserves
both Apostra's `media_buy_id` and the storefront's `adcp_media_buy_id`.
Tracked campaigns return
`directed.provenance: "connected_account"` with connection/account IDs,
provider, mirror freshness, and upstream media-buy identity.
## Connect and subscribe an account
First [connect the official adapter](/v2/guides/connecting-ad-platforms) and
[map its discovered account to an advertiser](/v2/concepts/ad-platform-accounts#after-connecting).
For an agent-driven setup:
1. Call `list_storefronts`, then `connect_storefront` to obtain the human OAuth
handoff URL.
2. Call `list_storefront_connections` and
`list_storefront_connection_accounts` to choose a buyable account.
3. Read `list_storefront_connection_account_mappings`. If needed, call
`map_storefront_connection_account_to_advertiser` with the intended
advertiser.
4. Call `subscribe_directed_campaigns` with the mapped connection and account
IDs to start the read-only mirror.
Organization/manager and publisher-identity accounts cannot be subscribed.
```bash theme={null}
curl -X POST \
"https://api.apostra.com/api/v2/buyer/storefront-connections/42/accounts/81/directed-campaigns/subscribe" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"advertiserId": "12345"
}'
```
Subscription immediately attempts a metadata sync. Active campaigns are
always included; terminal history is backfilled for one year by default.
Subsequent worker sweeps reconcile changes every 15 minutes by default. This
does not import delivery history.
The response includes `backfillStart`, `lastSyncedAt`, `lastSyncStatus`,
`lastSyncError`, and mirrored/retired/skipped counts. Read the same state later
with:
```text theme={null}
GET /api/v2/buyer/storefront-connections/{connectionId}/accounts/{accountId}/directed-campaigns/subscription
```
`backfillStart` is a fixed metadata-history boundary, not a progress meter. A
subscription remains durable when enumeration is unsupported or temporarily
unhealthy; the status reports `ERROR` and the worker retries. Existing valid
mirrors survive a failed or incomplete snapshot.
When a broad account read omits a known nonterminal campaign, Apostra
performs a bounded direct read before retiring its projection. A complete
terminal response preserves campaign and package history even if the provider
returns no packages. A failed or incomplete direct read retains the last valid
mirror.
To stop mirroring, call `unsubscribe_directed_campaigns`. Apostra pauses
the sweep and retires local projections; it does not delete upstream campaigns.
### Registered AdCP storefront sources
A buyer may project a registered third-party AdCP storefront source
into the same connection plane. Register source credentials, discover the
source account with `list_available_accounts`, map it to an advertiser, then
call `connect_adcp_storefront`:
```bash theme={null}
curl -X POST \
"https://api.apostra.com/api/v2/buyer/storefronts/42/sources/publisher-sales/adcp-connection" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"advertiserId": "12345",
"accountId": "provider-account-7",
"credentialId": "81"
}'
```
The response returns `connectionId` and `connectionAccountId`; use them with
the normal subscribe, subscription-status, campaign-read, and delivery-read
operations. The operation accepts only sources already registered on an active
storefront. It never accepts an arbitrary endpoint URL or raw secret.
For registered sellers, a snapshot is complete only when every requested page
returns boolean `pagination.has_more`, every cursor advances, every buy carries
a valid ISO currency, and any returned account ID matches the pinned account.
Incomplete snapshots never retire an existing projection.
## List and inspect mirrors
Use the shared campaign surface and compatibility mode filter:
```bash theme={null}
curl \
"https://api.apostra.com/api/v2/buyer/campaigns?mode=directed&advertiserId=12345" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
`GET /api/v2/buyer/campaigns/{campaignId}` returns the tracked campaign and its
single mirrored media buy. Discovery, execute, pause, reactivate, and ordinary
campaign mutations reject tracked projections.
To prioritize a fresh upstream snapshot without changing the provider account, send the
refresh-only compatibility request:
```json theme={null}
{ "mode": "directed", "refresh": true }
```
### Meta: observed automation controls during refresh
Tracked Meta campaigns that use Advantage+ audience targeting can return
provider-observed automation controls during refresh. The Meta adapter validates
and retains those controls in its provider readback evidence so a known control
does not make the complete refresh fail as unsupported. The shared directed
campaign response does not expose raw Meta automation controls, and they do not
appear in the package's canonical `targeting_overlay` or grant write
authorization. The tracked surface remains read-only, and the values reflect
provider state rather than an Apostra-authored targeting decision.
Active `individual_setting` controls are reported only when `advantage_audience: 1`
is also present; an active individual control without the automation anchor is not
representable and causes the upstream read or refresh to fail closed. All other
active automation dimensions (e.g. `creative_audience_pairing`) and any
malformed or unrecognized values are likewise not representable. Adapter-created
packages with an expected ledger use full durable product/request/confirmation
authorization rather than provider-observed readback.
## Relationship rollup
Scale lives on the provider-account relationship rather than the default
campaign list. Each account may return a `campaignRollup`:
| Field | Meaning |
| - | - |
| `campaignsTracked` | Non-retired tracked campaigns on the account |
| `campaignsActive` | Tracked campaigns currently active |
| `trackedSpend` | Provider-reported delivered spend across tracked campaigns in the trailing window |
| `managedSpend` | Delivered spend across canonical Apostra-managed campaigns in the trailing window |
| `managedShare` | `managedSpend / (trackedSpend + managedSpend)`; `null` when no spend was accounted |
| `currency` | Account ISO 4217 currency |
| `windowDays` | Trailing aggregation window |
Money never rolls up across accounts because accounts on one connection may
use different currencies.
## Presences: audiences and event sources
Connected accounts may expose first-party audiences and event sources as
account presences. Use
`GET /api/v2/buyer/storefront-connections/{connectionId}/accounts/{accountId}/presences`
or `list_account_presences`. This is a read-only discovery surface; it does not
sync an audience or authorize a provider mutation.
## Read delivery through the provider
Use the normal campaign delivery endpoint:
```bash theme={null}
curl \
"https://api.apostra.com/api/v2/buyer/campaigns/cmp_123/delivery?startDate=2026-01-01&endDate=2026-07-10" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Each call reads through to the connected provider's
`get_media_buy_delivery`. Raw payloads and time series are not cached. Only the
latest aggregate media-buy/package metrics, exact requested window, and refresh
time are persisted. Subscription and metadata sweeps never pull delivery.
If dates are omitted, the read requests the one-year window through yesterday.
Provider window limits may cause Apostra to split the request into
consecutive chunks before merging aggregate results.
## Errors and recovery
* Account or mapping not found: re-list the official connection and discovered
accounts, then use only returned IDs.
* `lastSyncStatus: "ERROR"`: inspect safe `errorCode`, `errorField`,
`errorReason`, and `upstreamCode` fields. Reconnect only for authentication or
authorization failures; retry provider-shape or availability failures.
* `mirrored: 0` with successful sync is an empty eligible history window, not a
failed backfill.
## Boundaries
* The tracked-campaign surface is subscription, mirror, refresh, presence,
relationship-rollup, and delivery read-only.
* Campaigns created or managed through Apostra use the canonical buyer
product and media-buy lifecycle, not a tracked-campaign write path.
* Change webhooks, scheduled delivery/export, invoice-grade reporting, and
BigQuery delivery history are not part of tracked-campaign mirroring.
* Availability never subscribes an account and never grants authority to
mutate its campaigns.
# Media-buy Frequency Caps
Source: https://docs.apostra.com/v2/buyer/campaigns/media-buy-frequency-caps
Apply an AdCP 3.2 frequency cap independently to each seller media buy in a V3 campaign
## Overview
Use `frequencyCap` on V3 `save_campaign` to limit exposure within each seller's
media buy. The seller maintains one counter across every participating package
in that media buy, following the AdCP 3.2 aggregate frequency-capping contract.
Existing campaigns are unchanged. The field is optional and is only available
while the campaign is a draft.
This is not a cross-seller campaign cap. Each seller maintains its own
counter. If a campaign buys from two sellers with a cap of three impressions
per individual, the same individual could receive three impressions from
each seller.
## Set the cap
Pass the cap when creating a campaign, or update it before launch:
```json theme={null}
{
"campaignId": "CAMPAIGN_ID",
"frequencyCap": {
"level": "mediaBuy",
"config": {
"maxImpressions": 3,
"per": "individuals",
"window": {
"interval": 7,
"unit": "days"
}
}
},
"idempotencyKey": "campaign-frequency-cap-v1"
}
```
| Field | Description |
| - | - |
| `level` | Must be `mediaBuy`. Other levels are rejected. |
| `config.maxImpressions` | Maximum impressions in the configured window. |
| `config.per` | Reach unit: `individuals`, `households`, `devices`, `accounts`, `cookies`, or `custom`. |
| `config.window` | Positive interval with a unit of `seconds`, `minutes`, `hours`, `days`, or `campaign`. |
Pass `frequencyCap: null` to clear the cap from a draft before accepting a
proposal. Omit the field to leave the current value unchanged.
## Seller and product eligibility
When a cap is present, `request_proposals` asks sellers for products that can
participate in the requested shared media-buy counter. A seller must declare
the AdCP 3.2 `media_buy.aggregate_frequency_capping` capability, and every
selected product must declare compatible `media_buy_support.frequency_cap`
support.
A seller proposal must also state the exact cap it is offering. If it leaves
the cap out or changes it, that proposal is not shown as a usable quote. The
seller's compatible products may still be available for a direct MediaBuy
request, which must confirm the cap when created.
The campaign launch rechecks those declarations against the exact selected
products. It fails before seller dispatch if support is missing or the cap is
outside a seller or product constraint.
## Cap levels
| Level | Counter scope | V3 campaign support |
| - | - | - |
| Campaign | Shared across every seller in the campaign | Not available. This requires cross-seller identity and counter coordination. |
| Media buy | Shared across participating packages bought from one seller | Available through `save_campaign.frequencyCap` with `level: "mediaBuy"`. |
| Package | Maintained for one package | Available on `save_media_buy.products[].targetingOverlay.frequency_cap` when the seller supports it. |
Selecting one seller does not change the declared level to `campaign`. It
remains a media-buy cap, which keeps the saved contract stable if cross-seller
campaign capping is added later.
## Read and update behavior
The campaign workspace returned by `save_campaign` and
`get(kind: "campaign")` includes the saved `frequencyCap`. The value is the
campaign's requested media-buy policy. Each seller's completed AdCP 3.2 create
response must echo the effective root `frequency_cap` before synchronous or
asynchronous creation is treated as capped. `get(kind: "media_buy")` then
returns `frequencyCap.status: "confirmed"` with the seller's effective cap.
Package caps are independent. Put the AdCP `frequency_cap` directly on one
product selection when only that package should count exposures:
```json theme={null}
{
"campaignId": "cmp_123",
"sellerId": "42",
"products": [
{
"productId": "sf1:42:reach",
"budget": 5000,
"targetingOverlay": {
"frequency_cap": {
"max_impressions": 3,
"per": "individuals",
"window": { "interval": 7, "unit": "days" }
}
}
}
],
"idempotencyKey": "package-frequency-cap-v1"
}
```
The package cap is sent only on that package. It is not combined with, or
promoted into, the MediaBuy-wide counter.
On Meta and Pinterest, a capped package must resolve to at most one offering
(one ad set or ad group). Each offering would otherwise have a separate
provider counter. If a package resolves to several offerings, the request
fails before creating the provider buy; separate the offerings into packages
or remove the package cap.
Package-cap shapes are seller-specific. V3 accepts the AdCP 3.2 cooldown
controls (`suppress` or deprecated `suppress_minutes`) and the
`max_impressions`/`per`/`window` control. The seller advertises package-cap
targeting through `media_buy.execution.targeting.frequency_cap` and must reject
any shape it cannot represent exactly.
Live cap changes are not available yet. A saved MediaBuy draft keeps the cap
it had when it was created, even if the campaign's draft cap changes later.
MediaBuys saved before this feature remain uncapped.
To use the new cap, remove the old draft, request proposals again, and create a
new MediaBuy. Once a proposal is accepted and bound to a MediaBuy, remove that
buy before changing the cap. Launch rejects a buy if its saved cap differs
from the cap used for the accepted proposal.
# Media Buy
Source: https://docs.apostra.com/v2/buyer/campaigns/media-buys
One AdCP transaction with one sales agent — platform-created or mirrored from a connected provider account
## Overview
A **Media Buy** represents one AdCP transaction with one sales agent. For
platform-managed campaigns, Apostra spawns media buys when a
[Campaign](/v2/object-guides/campaign) executes. During the tracked-campaign
alpha, Apostra can also project one storefront-managed media buy into one
read-only [tracked campaign](/v2/buyer/campaigns/directed-campaigns).
Campaigns managed by Apostra use the canonical buyer product and media-buy
lifecycle instead of a separate tracked-campaign write path.
When a campaign defines [channel groups](/v2/buyer/campaigns/channel-groups),
each media buy also preserves the coherent inventory selection it was created
for. The compiler creates one buy per channel group, seller, and settlement
currency, and reporting can roll delivery back up by that same group.
For platform-managed campaigns (`management: "managed"`), media buys are
spawned at execution, not before. A `DRAFT` platform-managed campaign has no
media buys. After `POST /campaigns/:id/execute`, the campaign holds a
`mediaBuys[]` array — one entry per channel group, sales agent, and settlement
currency (or per sales agent and currency when no groups are defined). A
`directed` campaign instead
always projects exactly one mirrored media buy and has no execute step.
Manual product staging via `create_media_buys` is available on campaigns with
`autonomy.inventorySelection: "manual"` or `"propose"` (the default). Campaigns
with `autonomy.inventorySelection: "automatic"` reserve composition for
`auto_select_products` and reject manual staging calls.
### Where a media buy sits in the hierarchy
A campaign fans out into media buys, each of which fans out into packages, each of which delivers:
```
Campaign
└── Media Buy (one per channel group, sales agent, and currency)
└── Package (one per product per pacing period)
└── Delivery (impressions, spend, clicks)
```
See [Package](/v2/buyer/campaigns/packages) for the per-product, per-pacing-period unit beneath each media buy.
## Product format identity
Products embedded in a media-buy read expose their URL-free canonical
`formatOptions` declarations. For a composed product, these declarations are
the only format identity returned; Apostra does not manufacture a legacy
`creativeFormats` value or an agent URL from the canonical kind. A directly
connected older seller may still return `creativeFormats` while its catalog is
migrating, but new integrations should read `formatOptions` and use the stable
`format_option_id` to select the exact option during media-buy creation.
For a wholesale product, Apostra captures `formatOptions` from the exact
discovery result you selected and carries that snapshot onto the media-buy line
item. It does not re-read a later public or account-private seller cache when
serving the buy. Historical wholesale buys created before snapshots were stored
may omit the optional field; re-run discovery to create a buy with current
requirements.
Media buys created from an older composed-product row can temporarily retain
the exact legacy `creativeFormats` readback until that row is refreshed. This
compatibility path does not affect newly composed products.
## Budget
A media buy's budgets — its per-product budgets and the package budgets beneath it — are **gross**: fee-inclusive, in the buyer's currency, the same denomination as the campaign's `budget.total`. The media/fee split is derived at the fee terms locked when the buy was created, and media buy reads return it read-only:
```json theme={null}
{
"mediaBuyId": "mb_abc123",
"budget_denomination": "gross",
"budget_breakdown": {
"media_budget": 23000,
"fee_amount": 2000,
"fee_rate_percent": 8,
"effective_gross_cpm": 20.11
}
}
```
* `budget_denomination` — always `"gross"`.
* `budget_breakdown.media_budget` — the portion of the gross budget that buys media.
* `budget_breakdown.fee_amount` — Apostra fee inside the gross budget.
* `budget_breakdown.fee_rate_percent` — the fee rate locked when the buy was created. Budget updates on a live buy re-split at this rate; later contract changes affect new buys only.
* `budget_breakdown.effective_gross_cpm` — the buy's gross budget ÷ impression goal × 1000: the all-in price per thousand impressions, so "budget ÷ CPM = impressions" holds on the numbers you operate on (for a buy priced at a single fixed seller CPM, it works out to `seller CPM ÷ (1 − fee rate)`). `null` when the buy has no positive impression goal or no gross budget.
`budget_breakdown` is informational — you set and update gross budgets only. Legacy buys created before fee terms were locked carry neither `budget_denomination` nor `budget_breakdown`, and report delivered spend net, as the seller reported it. On all other buys, delivered spend on buyer surfaces is gross too, so delivery compares directly against these budgets. Sellers receive and report the media amount with the fee already carved out, and never see your fee rate. See [Budgets and fees](/v2/concepts/budgets-and-fees).
## Status
A media buy's `status` is one of:
| Status | Meaning |
| - | - |
| `DRAFT` | Created but not yet submitted |
| `PENDING_APPROVAL` | Submitted; awaiting seller approval |
| `INPUT_REQUIRED` | Seller needs additional information before approval can proceed |
| `ACTIVE` | Live and delivering |
| `PAUSED` | Spend halted; resumable |
| `COMPLETED` | Flight finished or fully delivered |
| `CANCELED` | Stopped before completion |
| `FAILED` | Submission or execution error |
| `REJECTED` | Seller declined the buy |
| `ARCHIVED` | Soft-deleted |
This is Apostra buyer rollup status — a coarser, derived view of the
per-buy AdCP wire status (`pending_creatives`, `pending_start`, `active`, …) the
sales agent returns, surfaced here as `adcp_status` on
[Get media buy status](/v2/buyer/campaigns/tasks/get-media-buy-status).
`pending_creatives` and `pending_start` both collapse to `PENDING_APPROVAL`
here; `active` → `ACTIVE`, and so on. See
[Media buy lifecycle](/v2/concepts/media-buy-lifecycle) for the full mapping.
### Most-restrictive rule
For a multi-agent campaign, the surfaced status is the **most restrictive** across all agents. For example, if one media buy is `ACTIVE` and another is `PENDING_APPROVAL`, the campaign reports `PENDING_APPROVAL`. `INPUT_REQUIRED` signals that a seller is blocked waiting on more information.
### How status flows
Status updates reach you two ways:
* **Webhooks** from sales agents — preferred, near-real-time. No polling needed.
* **Polling** via `GET /api/v2/buyer/campaigns/:id/media-buy-status`, which queries each agent directly and persists any changes.
```bash theme={null}
curl https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/media-buy-status \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Polling is most useful right after execution, while you wait on publisher approvals. Once webhooks are flowing, prefer them — see [Get media buy status](/v2/buyer/campaigns/tasks/get-media-buy-status).
If you attach a creative while a seller is reviewing the media buy, Apostra
automatically retries delivery after approval. You do not need to detach,
reattach, or resubmit it. Retry can continue for up to 24 hours; if delivery
still has not completed, contact the seller or Apostra support. Some sellers
return their own creative ID after delivery. Apostra displays it when
available, but a missing seller ID alone does not mean delivery failed.
### Creative format fit
Creative format fit is evaluated for each product, not once for a campaign.
When a Creative is attached or considered for a media buy, Apostra compares
its canonical `formatKind` and `formatParams`, plus inspected uploaded-media
facts when available, with that product's declared format option. The response
is `fits`, `fits_with_adaptation`, `insufficient_facts`, or `nothing_fits`.
If the product requires a size, duration, codec, or container that the Creative
does not declare and its uploaded asset has not established, Apostra returns
`insufficient_facts` and names what is missing. It cannot confirm that an
uninspected or parameter-less Creative fits, but the attachment proceeds
unconfirmed. Required adaptations are named as
`resize`, `letterbox`, `trim`, or `transcode` with the target option's
parameters. `nothing_fits` names the closest option and what differs. An
attachment does not perform adaptations. A Creative that needs adaptations is
forwarded as-is for seller review, and the attachment records the required
adaptations in its audit record.
### Why a storefront held or rejected your buy
A `PENDING_APPROVAL` status means the storefront's operator is reviewing your buy
before it reaches inventory; a rejection carries a buyer-safe reason. Sellers
judge buys against **Brief Acceptance** (the categories, advertisers, and rules
they will and won't carry). A deterministic hard finding supplies a strong
operator-facing rejection signal, while an ambiguous or AI-judged conflict
escalates or fails closed. Clearly on-policy buys auto-forward; ambiguous
results, evaluator errors, and deterministic hard findings enter human review.
Understanding the seller-side model helps your agent interpret a hold or rejection
and adjust — see [AI Business Rules](/v2/concepts/ai-business-rules).
For an exact own-supply campaign managed by the Media Company that owns the
Storefront, revision-fenced launch confirmation and authenticated
non-cancellation updates are Storefront media-buy decisions. They do not create
a second operator approval task. The source can still return a pending or
rejected status, and every validation, readiness, financial, and safety gate
remains in force.
## Why isn't my buy live?
For storefront-routed buys, the platform derives a **why-visibility annotation** from its forwarding records and exposes it in three places: the single-buy lookup below, the nested `mediaBuys[]` in `GET /campaigns/:id` (camelCase fields), and the [media-buy status poll](/v2/buyer/campaigns/tasks/get-media-buy-status) (snake\_case fields). The fields are present only when forwarding state exists; a buy transacted directly with a sales agent carries none.
### One-call lookup
```bash theme={null}
curl https://api.apostra.com/api/v2/buyer/media-buys/mb_abc123 \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```json theme={null}
{
"mediaBuy": {
"mediaBuyId": "mb_abc123",
"name": "CTV — Premium News",
"status": "PENDING_APPROVAL",
"pendingAt": "salesagent",
"pendingReason": "awaiting_source_moderation",
"pendingSince": "2026-07-03T09:12:00Z",
"sourceMessage": "Awaiting manual moderation",
"forwardedAt": "2026-07-03T09:11:58Z",
"buyerReference": "sf:42:sf_mb_1783031864469_8pbc8mmr",
"startTime": "asap",
"endTime": "2026-07-31T23:59:59Z",
"createdAt": "2026-07-03T09:10:00Z",
"updatedAt": "2026-07-03T09:12:01Z"
}
}
```
### `pendingReason` — what the buy is waiting on
`pendingReason` is a **derived annotation, never a status** — it explains a wait; the buy's `status` is unchanged. It is derived per source leg and rolled up to the most-blocking wait. See [Media buy lifecycle](/v2/concepts/media-buy-lifecycle#pendingreason-whose-side-owns-the-wait) for the full vocabulary and whose side owns each wait.
### `errorCode` — structured failure codes
When forwarding failed or the buy was rejected, `errorCode` carries one of a small buyer-safe set, and `errorOwner` says who owns the fix:
| `errorCode` | `errorOwner` | Meaning | What to do |
| - | - | - | - |
| `product_no_longer_available` | `platform` | A selected product no longer resolves on the storefront (catalog drift) | Re-run discovery and rebuild the buy with current products |
| `source_rejected` | `seller` | The inventory source declined the buy on its merits | Read `sourceMessage`, address the reason, resubmit |
| `storefront_rejected` | `seller` | The storefront operator rejected the buy in review | Read `sourceMessage` (the reviewer's note), adjust, resubmit |
| `source_unavailable` | `seller` | The source could not be reached or went silent — not a refusal | Retry later; escalate with your `buyerReference` if it persists |
| `source_configuration_error` | `seller` | The source is not configured to process this media buy | Ask the seller to correct the source configuration; retrying the same request will not help |
| `product_configuration_error` | `seller` | The selected product contains stale or unusable configuration | Re-run discovery and rebuild the buy with a current product; ask the seller to correct the product if it remains available but still fails |
| `invalid_request` | `buyer_input` | The request needs a correction from you | Fix the flagged input and resubmit |
| `quote_expired` | `platform` | The held cross-currency rate expired before the buy forwarded | Re-discover and submit a new buy at the current rate |
| `platform_error` | `platform` | A platform-side fault | Contact support with your `buyerReference` |
`sourceMessage` is the first detail to read. When present, it contains a sanitized explanation from the source or reviewer, or buyer-safe correction guidance from the storefront forwarder.
### `buyerReference` — the handle to quote
`buyerReference` (`sf::`) identifies this buy's exchange with the seller. Quote it, together with `forwardedAt`, when contacting the seller or Apostra support about a stuck buy — it is the reference both sides can look up.
### Media-buy identifiers and source references
A storefront-routed buy can have a second identifier minted by the inventory
source that executes it. Successful `create_media_buy` and `update_media_buy`
responses may include buyer-safe pointers in `ext.source_references`:
```json theme={null}
{
"ext": {
"source_references": [
{
"source_name": "Example Sales Agent",
"media_buy_id": "mb_upstream_123"
}
]
}
}
```
* A sales-agent source leg includes its source name when available and upstream
`media_buy_id`.
* An ad server source leg includes the upstream `media_buy_id` without naming
the internal source.
* Linked and modular source legs are omitted. Their underlying source topology
is intentionally private.
Use a returned source reference when the counterparty asks for the identifier
in its own system. Continue to use `mediaBuyId` with Apostra APIs and
`buyerReference` when asking Apostra or the storefront to trace the full
exchange. The field is absent when no eligible upstream reference is available.
### Transition notifications
The same facts push to your [notification stream](/v2/guides/notifications) as `media_buy.*` events, fired once per transition (never per retry):
| Event | Fired when |
| - | - |
| `media_buy.forward_failed` | The first forward attempt failed (payload carries `errorCode`, and `pendingReason` when the platform is retrying or a correction is needed) |
| `media_buy.awaiting_source_moderation` | The source accepted the buy asynchronously and is moderating it |
| `media_buy.source_rejected` | The source declined the buy (payload carries the sanitized reason) |
| `media_buy.stuck` | The buy never resolved at the source (timeout or task failure) and needs attention |
Each payload includes `mediaBuyId`, `buyerReference`, and the applicable `pendingReason` / `errorCode` / `errorOwner` / `sourceMessage`, so a reacting agent does not need a second call to learn why.
## Optimization goals
Optimization goals are applied at the media-buy level — they propagate to every package in the media buy at execution time. Each goal is either **event-based** (optimize against tracked conversions) or **metric-based** (optimize against seller-native delivery metrics).
```json Event goal — optimize for tracked conversions theme={null}
{
"kind": "event",
"event_sources": [
{ "event_source_id": "website_pixel", "event_type": "purchase", "value_field": "value" }
],
"target": { "kind": "cost_per", "value": 25.0 }
}
```
```json Metric goal — optimize for seller-native delivery metrics theme={null}
{
"kind": "metric",
"metric": "completed_views",
"target": { "kind": "cost_per", "value": 0.08 }
}
```
### Target kinds
| Target kind | Meaning |
| - | - |
| `cost_per` | Target CPA (events) or CPM-like rate (metrics) |
| `per_ad_spend` | Target ROAS — value per dollar spent (events only) |
| `maximize_value` | No target — maximize total event value within budget |
| `threshold_rate` | Maintain a minimum rate of the metric (metrics only) |
This level does not yet support an explicit `strength` on a `cost_per`/`per_ad_spend` target — some sellers (e.g. Meta) require a specific strength (`cap` for `cost_per`, `floor` for `per_ad_spend`) and reject a goal here that omits it. Set the goal on the campaign's `performanceConfig.optimizationGoals` instead (see [Optimization goal target strength](/v2/object-guides/campaign#optimization-goal-target-strength-performanceconfig)) when targeting such a seller.
### Supported metrics
`clicks`, `views`, `completed_views`, `viewed_seconds`, `attention_seconds`, `attention_score`, `engagements`, `follows`, `saves`, `profile_visits`, `reach`.
`impressions` is a delivery rollup — something a package reports, not a metric you can target (see [Delivery metrics](/v2/object-guides/campaign#packages)). For a reach or CPM-style goal, target `metric: "reach"`:
```json Reach goal — target unique reach at a minimum frequency theme={null}
{
"kind": "metric",
"metric": "reach",
"reach_unit": "individuals",
"target_frequency": { "min": 3, "window": { "interval": 7, "unit": "days" } }
}
```
`reach_unit` is required when `metric` is `reach` — one of `individuals`, `households`, `devices`, `accounts`, `cookies`, `custom`, and must be a unit the product declares support for. `target_frequency` is optional — omit it to let the seller maximize unique reach without a frequency constraint.
This is the `mediaBuys[].optimization_goals` field on an *executed* media buy (snake\_case, the AdCP wire format — `reach_unit`/`target_frequency` included). It is distinct from `performanceConfig.optimizationGoals` (camelCase), which sets goals when *creating* a performance campaign and does not yet support `reach_unit`/`target_frequency` — conversely, `performanceConfig.optimizationGoals` supports an explicit target `strength`, which this level does not yet support.
## Goal commitment
Every media buy booked after commitments were recorded carries a
`goalCommitment`: what the seller committed to for your goal at the moment
the buy was created. It is read off terms, never prose, and it never changes
on the row it was booked on; a later change to the buy books a new row with
its own commitment.
| Field | Meaning |
| - | - |
| `source` | Where the answers came from: `proposal` when at least one product on the buy keeps the accepted proposal version's answer; `campaign` when every answer was derived from the campaign's goals and the products' own pricing and delivery terms. |
| `proposalVersionId` | The accepted proposal version the buy was booked from, whenever there was one. It stays on the buy even when `source` reads `campaign` because none of the buy's products matched that version's answers, so you can always trace what was accepted. |
| `kind` | `guaranteed`, `best_effort`, or `report_only`, using the same rules as a proposal's commitment (see [Read what a seller committed to](/v2/setup/v3/buyer-workflows#read-what-a-seller-committed-to)). The buy is only as committed as its least committed product. |
| `askedGoal` | The goal you stated on the campaign when the buy was booked, or `null` when there was none. |
| `answeredTarget` | The target the products' terms commit or aim at for that goal, of the same kind as your ask when they answer it: the worst across products, meaning the highest cost per unit, the lowest rate, or the lowest return. `null` when nothing answered a number. |
| `meetsAskedTarget` | Whether every product answers your target, judged by its kind: a cost per unit is met at or under your ask, a rate or a return on ad spend at or above it. `false` when any product misses it or did not answer with the same kind, `null` when your goal carries no target or a maximise target, which has no number to meet. |
| `goalAnswers` | One answer per product: pricing model and fixed price, delivery type, measurement terms, and its own commitment kind. |
| `goalAnswers[].storefrontId` | The Storefront the answered line resolves to, fixed when the answer was booked. A later change to the draft keeps that line's answer for exactly that Storefront; two Storefronts' like-named products never share one. Present on every booked answer whose line resolved to a Storefront, campaign-derived or copied from an accepted proposal alike; absent only on a line with no resolved Storefront. |
A buy created without a proposal still gets a commitment from its own terms:
a fixed CPC on a guaranteed product reads `guaranteed` even if the campaign
stated no goal, so the price you are paying per result is always on record.
The commitment always answers for every product on the buy. When you accept a
proposal into a draft that already holds other products, each product the
proposal answered keeps that answer and every other product is answered from
its own pricing and delivery terms, so `kind` is the buy's weakest product,
never the proposal's alone. A proposal version can only be booked onto the
campaign it was quoted for; accepting a version that does not belong to the
campaign fails the booking rather than recording a commitment that is not
yours.
A product booked on several lines of the same buy gets one answer per line.
Each proposal answer goes to one line, and a line the proposal did not answer
is answered from its own booked terms, so one line's price is never copied onto
another.
The same `goalCommitment` is returned on the V3 `media_buy` reads (`get` and
`search`) and on each media buy in a campaign read with the `mediaBuys`
include. It is read-only. `get` returns at most 50 line answers, the same
bound as the line items it lists; `search` and the campaign include return at
most 20 per buy. When a buy has more answers than a read carries, that read
sets `goalAnswersTruncated: true`.
## Reading and tuning media buys
`GET /api/v2/buyer/media-buys/:mediaBuyId` returns the selected media buy's
`optimizationGoals` alongside its status and diagnostic fields. Use the
campaign read when you also need packages, products, or delivery details.
Because media buys aren't created directly, you act on them through `PUT /api/v2/buyer/campaigns/:id` using the `mediaBuys[]` array. Each entry targets one media buy by `mediaBuyId` and carries an `action`.
When lowering a campaign's `budget.total` below what live media buys have already allocated, include explicit `mediaBuys[].packages[].budget` reductions in the same request — the update is validated against the projected post-update allocation and applied atomically. A budget decrease that leaves live buys allocating more than the new total is rejected with `INSUFFICIENT_MEDIA_BUDGET`. See [Reducing campaign budgets](/v2/buyer/campaigns/tasks/update-campaign#reducing-campaign-budgets).
| Action | Effect |
| - | - |
| `update` | Default — modify budget, pacing, optimization goals, or creatives |
| `cancel` | Cancel a running media buy |
| `delete` | Archive the media buy |
### Update budget and optimization goals
```json Update budget + optimization theme={null}
{
"mediaBuys": [
{
"action": "update",
"mediaBuyId": "mb_abc123",
"packages": [
{ "packageId": "pkg_1", "budget": 15000, "pacing": "even" }
],
"optimization_goals": [
{ "kind": "metric", "metric": "completed_views", "target": { "kind": "cost_per", "value": 0.08 } }
],
"updated_reason": "Mid-flight optimization based on early performance"
}
]
}
```
```json Cancel one media buy theme={null}
{
"mediaBuys": [
{ "action": "cancel", "mediaBuyId": "mb_abc123", "reason": "underperforming vs threshold" }
]
}
```
Always confirm `optimization_goals` with the buyer before changing them — silent goal changes break optimization continuity.
### Cancel a single package
To cancel one package without touching the rest of the media buy, pass `packageIds` on the `mediaBuys[]` entry:
```json theme={null}
{
"mediaBuys": [
{
"action": "cancel",
"mediaBuyId": "mb_abc123",
"packageIds": ["pkg_2"],
"reason": "format not supported on this device mix"
}
]
}
```
See [Package](/v2/buyer/campaigns/packages) for what each package represents.
### Override creative attachment
Omitting `creative_ids` preserves the buy's existing creative assignments without revalidating, resyncing, or forwarding any creative data. Include `creative_ids` only when you intend to change which creatives are attached.
```json theme={null}
{
"mediaBuys": [
{
"action": "update",
"mediaBuyId": "mb_abc123",
"creative_ids": ["cr_video_15s", "cr_video_30s"]
}
]
}
```
* **Omit `creative_ids`** to leave existing assignments unchanged. The platform does not fetch, validate, sync, or forward creative data for the buy in this case.
* **Explicit `creative_ids`** replaces the buy's current assignments with exactly the provided list. Each ID must already be linked to the campaign and match a format accepted by the media buy's products. A validation error is returned if any ID fails this check — the field is not silently filtered.
* **`"creative_ids": []`** explicitly clears all creatives from the media buy.
* `creative_ids` is only valid with `action: "update"`. It is rejected with `cancel` or `delete`.
### Package flight-date changes
Updating a package's `startTime` or `endTime` is rejected with `CAPABILITY_NOT_SUPPORTED` when **both** of the following are true: the target package has no persisted flight dates, and the seller has not yet returned action metadata (`valid_actions` or `available_actions`) for this buy. Otherwise, the date change proceeds.
This guard prevents forwarding a date change to a seller that has not declared what mutations it supports. It typically means the buy has not yet received a complete seller response — wait for the seller to confirm the buy is active and has returned its capabilities before adjusting package dates.
### Pausing never waits for seller approval
A buyer pause reaches the seller's ad server immediately, including on a
storefront that reviews every change manually and on guaranteed buys. Pausing
suspends delivery only: it does not change the buy's budget, flight dates, or
contract, and the buyer alone carries the cost of the buy going dark. The
seller is notified that the pause took effect but is not asked to approve it.
This applies to an update whose only change is the pause. A pause combined
with any other change (a budget, date, or package edit) is an ordinary update
and follows the storefront's approval rules. See
[pause media buy](/v2/buyer/campaigns/tasks/pause-media-buy) for how to tell
that delivery actually stopped.
### Pausing on each kind of storefront
A pause is never refused because an earlier change to the same buy is still
pending, for example a budget change waiting for the seller's approval. The
earlier change keeps its place; when it is applied later, the buy stays paused.
* **Storefronts that forward to a sales agent**, including Apostra-hosted
storefronts on Google Ad Manager: the pause goes straight to the seller's
sales agent.
* **Ad-platform storefronts** (for example Google Ads, Amazon, Spotify, Meta,
TikTok): the pause goes straight to the ad platform, whether you use
Apostra's [pause media buy](/v2/buyer/campaigns/tasks/pause-media-buy) or
call the storefront's AdCP `update_media_buy` with `paused: true` and nothing
else.
* **Meta:** if another change to the same Meta campaign is still being written
to Meta, the pause can wait up to 20 seconds for it to finish. If that change
is still running after the wait, the pause is refused with a retryable
`CONFLICT` (reason `media_buy_update_in_progress`); retry the pause.
Whenever a seller or ad platform refuses or fails a pause, you get an explicit
error, never a silent success, and Apostra's operations team is alerted
because the buy may still be delivering.
### When a seller is still applying a pause
Some sellers answer a pause with an AdCP "accepted, still processing" status
(`submitted` or `working`) instead of confirming it. Delivery has not stopped
yet, so Apostra does not show the buy as paused:
* The media buy stays `ACTIVE` and its `pauseRequestedAt` (on
`GET /media-buys/:mediaBuyId`) records when the pause was first requested.
A single-buy [pause](/v2/buyer/campaigns/tasks/pause-media-buy) returns
`202 Accepted`.
* When the seller reports the buy paused, Apostra moves it to `PAUSED` and
clears `pauseRequestedAt`.
* A [campaign pause](/v2/buyer/campaigns/tasks/pause-campaign) waiting on such
a seller returns `503` and the campaign stays `ACTIVE`. It becomes `PAUSED`
on its own once every media buy confirms.
* If the seller has not confirmed after two hours, Apostra's operations team is
alerted once. Apostra never sends the pause again on its own; pausing again
is your call and is safe.
A resume works the same way in reverse. When a seller answers a resume with
"accepted, still processing", the buy stays `PAUSED`, because it is not
delivering yet:
* A single-buy [reactivate](/v2/buyer/campaigns/tasks/reactivate-media-buy)
returns `202 Accepted` with `resumeRequestedAt`.
* A [campaign reactivate](/v2/buyer/campaigns/tasks/reactivate-campaign)
waiting on such a seller returns `503` with `reason`
`campaign_reactivate_requested` when those buys are all that is outstanding.
* The buy becomes `ACTIVE` on its own once the seller reports it delivering,
and so does its campaign.
### Resuming a paused media buy
A resume on its own restores terms the seller already approved, so it is not
treated as a material change:
* **Storefronts that forward to a sales agent** send a resume on its own
straight to the seller's ad server, without asking the seller to approve it.
* **Ad-platform storefronts with manual review** (for example Google Ads,
Amazon, or Spotify) review every change, so a resume still waits for the
seller there.
* **A resume combined with any other change** (a budget, date, or package edit)
is an ordinary update and follows the storefront's approval rules on every
storefront.
See [reactivate media buy](/v2/buyer/campaigns/tasks/reactivate-media-buy) for
the responses.
### Updating a paused media buy
A `PAUSED` media buy accepts the same updates as an `ACTIVE` one — budget,
flight dates, name, packages, and creatives. Pausing halts delivery; it does
not lock the buy against edits.
* **Budget changes still require seller approval**, exactly as they would on
an `ACTIVE` buy. The materiality classification that decides whether a
change needs approval does not special-case `PAUSED` — any budget change is
material regardless of the buy's current status.
* **Approving an update never resumes the buy.** Once a queued change is
accepted, the newly-activated values (budget, dates, etc.) go live, but the
buy's status is preserved as `PAUSED` — approval changes what the buy is
worth, not whether it's running. The only way to move a buy from `PAUSED` to
`ACTIVE` is [reactivate media buy](/v2/buyer/campaigns/tasks/reactivate-media-buy)
(or a campaign-level [reactivate](/v2/buyer/campaigns/tasks/reactivate-campaign)).
* **Updates on a paused buy still propagate to the seller.** Submitting a
change against a `PAUSED` buy is not a local-only edit — it queues exactly
the same way it would for an `ACTIVE` buy. See
[Pending changes: what is live vs what you asked for](/v2/concepts/media-buy-lifecycle#pending-changes-what-is-live-vs-what-you-asked-for)
for how to read `pendingChange` while an update on a paused buy is in
flight.
## Cascade behavior
Actions on the parent campaign cascade down to its media buys and packages:
* **Campaign pause** halts every media buy and package.
* **Campaign reactivate** brings them back to `ACTIVE` (post-flight if dates allow).
* **Creative manifest update** re-syncs to every media buy that uses the format.
* **Property list update** propagates to packages without a full re-execute.
* **Frequency caps** on the campaign or advertiser are enforced across all media buys.
* **Per-buy pause/reactivate** — [pause a single media buy](/v2/buyer/campaigns/tasks/pause-media-buy) and [reactivate a single media buy](/v2/buyer/campaigns/tasks/reactivate-media-buy) are the non-cascading counterparts: each touches exactly the targeted media buy, never the parent campaign row and never a sibling media buy. Use these when only one buy on a multi-agent campaign needs to stop or resume; use the campaign-level pause/reactivate above when the whole campaign should move together.
## Failures and debug
When a media buy fails to execute — a publisher rejection or an ADCP error — `POST /api/v2/buyer/campaigns/:id/execute` returns structured `errors[]`:
```json theme={null}
{
"campaignId": "cmp_987654321",
"previousStatus": "DRAFT",
"newStatus": "ACTIVE",
"success": false,
"errors": [
{
"mediaBuyId": "mb_abc123",
"salesAgentId": "agent_xyz",
"message": "Sales agent returned 422: invalid format for product"
}
]
}
```
Pass `debug: true` on execute to include the full ADCP request, response, and A2A debug logs in each error entry.
## Related concepts
The parent media plan that spawns media buys at execution
One per product per pacing period, beneath each media buy
Poll live ADCP status across sales agents
The endpoint that reads and tunes media buys
# Package
Source: https://docs.apostra.com/v2/buyer/campaigns/packages
One product per pacing period beneath a media buy — carries its own budget, pacing strategy, bid price, and optimization goals
## Overview
A **Package** is the delivery unit beneath a [Media Buy](/v2/buyer/campaigns/media-buys): one package per product, multiplied by the number of pacing periods. Packages are created when a media buy executes — like media buys, they are not directly creatable. Each package carries its own budget, pacing strategy, bid price, and optimization goals, and reports its own delivery metrics.
Packages are spawned at execution. A product selected on a `DRAFT` campaign becomes one package per pacing period once the campaign executes and the media buy submits to ADCP.
### Where a package sits in the hierarchy
```
Campaign
└── Media Buy (one per sales agent)
└── Package (one per product per pacing period)
└── Delivery (impressions, spend, clicks)
```
## Per-package fields
| Field | Type | Notes |
| - | - | - |
| `packageId` | string | Stable for the life of the package. Pass it when updating or pausing a single package. Opaque: see [Package identity and the id numbering scheme](#package-identity-and-the-id-numbering-scheme) before parsing it for anything |
| `budget` | number | Budget allocated to this package — gross (fee-inclusive), like every buyer budget. See [Budgets and fees](/v2/concepts/budgets-and-fees) |
| `pacing` | enum | Pacing strategy: `even`, `asap`, `front_loaded` |
| `bid price` | number | Per-package bid |
| `optimizationGoals` | array | Inherited from the media buy at execution (event- or metric-based) |
## Meta campaign budget allocation
Set `budgetAllocation.mode` to `seller_optimized` only when creating a new Meta media buy from one or more explicit qualified Meta product selections through `create_media_buys` or `save_media_buy`. All selected products must be on the same Meta storefront and resolve to the same effective channel group. An omitted `channelGroupId` resolves to the campaign's only group. Products in different channel groups need separate media buys. It requires an existing campaign with a saved budget total and no existing cart selections or draft media buy for that campaign. The Meta storefront must not be paused or require seller approval. Proposals, mixed storefronts, and non-Meta selections are refused before a draft changes. You cannot add or change `budgetAllocation` on an existing media buy.
When using `save_media_buy` to create a Meta buy from selected products, choose `budgetAllocation.mode` explicitly. Use `fixed` for a separate budget on each ad set, with a positive `products[].budget` on every selection. Use `seller_optimized` for one shared campaign budget, with an `optimization_goals` entry supported by every selected product. If the request does not say which budget level the buyer wants, ask before creating the buy. A `seller_optimized` `save_media_buy` request must omit both `products[].budget` and the media buy's top-level `budget`: Meta uses the campaign's saved total. Returned packages show no invented share of that total. A single Meta campaign can contain multiple budget-less ad sets; Meta distributes the total across them.
The v2 `create_media_buys` operation still accepts an omitted `budgetAllocation` for its existing per-package behaviour, including funding a package without a stated budget from the campaign remainder. A `seller_optimized` request on either operation must not include a package budget.
`seller_optimized` allocation snapshots the saved campaign total when the new Meta buy is created and sends that total through the canonical Meta route. A later campaign-budget edit does not change an existing buy. Existing media buys keep their original allocation behaviour. Storefronts that require seller approval are refused.
## Package identity and the id numbering scheme
Every package carries a `packageId` that is stable for the life of the package, and it is what you pass in `packageIds` or `packages[]` when you cancel or update one package (see [Cancel a single package](#cancel-a-single-package) below). Storefront-minted ids follow this shape:
```
sf_pkg__
```
`N` is a **one-based position across the whole dispatch**, not a period number, and it is not stable across media buys. On a buy with 2 products over 3 pacing periods, `_1` through `_6` cover both products: the first three belong to one product, the next three to the other.
Treat `packageId` as opaque. Never parse the trailing number to find a package's pacing period, and never assume the same suffix means the same period across two different media buys. Read `pacingPeriod.index` and `pacingPeriod.label` on the package itself instead.
On the first product of a buy the suffix happens to line up with the period index, which makes a parsing shortcut look correct when you spot-check it. It is a coincidence of the ordering: on the second product the same suffix is off by the number of periods.
### Going from a description to a packageId
A buyer or their agent typically has a description, not an id: "the display package ending 2026-08-11." [Get media buy packages](/v2/buyer/campaigns/tasks/get-media-buy-packages) resolves that directly, no seller-side export needed. Match on `productName` plus `endTime` (or `pacingPeriod.label`), then take `packageId`:
```bash theme={null}
curl https://api.apostra.com/api/v2/buyer/media-buys/mb_ETBn4gJ9Wu/packages \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```json theme={null}
{
"mediaBuyId": "mb_ETBn4gJ9Wu",
"isPaced": true,
"packageCount": 2,
"packages": [
{
"packageId": "sf_pkg_sf_mb_1783618937177_43ycbp7b_1",
"productId": "prod_display_300x250",
"productName": "Display Run of Site",
"status": "active",
"pacingPeriod": { "index": 1, "label": "Week 1" },
"startTime": "2026-08-01T00:00:00Z",
"endTime": "2026-08-07T23:59:59Z",
"budget": 5000,
"budgetCurrency": "USD",
"pacing": "even"
},
{
"packageId": "sf_pkg_sf_mb_1783618937177_43ycbp7b_2",
"productId": "prod_display_300x250",
"productName": "Display Run of Site",
"status": "active",
"pacingPeriod": { "index": 2, "label": "Week 2" },
"startTime": "2026-08-08T00:00:00Z",
"endTime": "2026-08-11T23:59:59Z",
"budget": 5000,
"budgetCurrency": "USD",
"pacing": "even"
}
]
}
```
Both packages are the same product, so `productName` alone does not pick one. The `endTime` does: the second entry ends `2026-08-11T23:59:59Z`, so its `packageId` (`sf_pkg_sf_mb_1783618937177_43ycbp7b_2`) is the one to send. Match on the field, not on the trailing number.
### Packages without a recorded flight window
`startTime`, `endTime`, and `pacingPeriod` are absent on packages created before the platform started retaining this identity, and on any media buy that was never split across periods. Expect this to be the common case on buys that are already live: no package created before the change carries a period, and most carry no window either. The platform now keeps the flight window it requested even when the seller's response omits it, but it cannot reconstruct one for a package already stored without it, so the gap is permanent for those packages.
The periods themselves are still readable on the campaign, including each period's label and dates. What is missing is only the link from a package to its period.
For those packages, `productName` plus `budget` sometimes narrows it to one. Often it will not: a schedule that gives several periods the same budget gives their packages identical budgets, so same-product siblings come back indistinguishable, and nothing this API returns separates them. Those packages cannot be addressed individually, and the seller is the only remaining route. Check for the presence of `startTime`/`endTime` before matching on them, and treat two identical entries as unresolved rather than picking one: pausing the wrong period is worse than asking.
### Pacing strategies
| Strategy | Behavior |
| - | - |
| `even` | Spread spend evenly across the package window |
| `asap` | Spend as fast as inventory allows |
| `front_loaded` | Weight spend toward the start of the window |
### Which pacing a package gets
Every package is sent to the seller with one pacing value, chosen in this order:
1. The pacing you set on the product line (`products[].pacing` on the media buy), when you set one.
2. Otherwise, the campaign's `budget.pacing`. `EVEN` is sent as `even`, `ASAP` as `asap`, and `FRONTLOADED` as `front_loaded`.
3. Otherwise, `even`, the default that AdCP defines for a budgeted buy.
A campaign created with `"budget": { "pacing": "EVEN" }` therefore sends `even` on every package unless a product line overrides it. A product line that sets `front_loaded` or `asap` sends that value to the seller unchanged.
The package's `pacing` field records the pacing the seller applies: the value sent, or `even` when the field was left off. If the seller returns a different pacing, the package shows the seller's value. It is absent when there is no record of what the seller was sent, for example on some packages created before this behavior.
**When the pacing field is left off.** AdCP treats a missing pacing as `even`, so leaving the field off never drops an `even` instruction. Apostra leaves it off in two cases:
* **Meta ad accounts.** The Meta integration does not accept a pacing field yet, so an `even` pacing is not sent to Meta, whether it came from the product line, the campaign or the default. Meta's standard delivery spreads spend evenly.
* **A seller Apostra cannot identify.** When a package is added to a live media buy and the seller behind it cannot be identified, `even` is left off in case that seller rejects the field. An `asap` or `front_loaded` pacing is still sent.
**Meta and an `ASAP` or `FRONTLOADED` campaign.** Meta cannot take these either. The media buy still launches and runs on Meta's standard even delivery; it is not refused. The execute response carries a `pacing_not_applied` warning for that media buy, naming each product, the pacing you asked for (`requestedPacing`), the pacing Meta applies (`appliedPacing: even`) and why. The package records `even`. If you set `asap` or `front_loaded` on a Meta product line yourself, Meta refuses the media buy with an unsupported-feature error instead, because that is an explicit instruction for that package.
Changing the campaign's `budget.pacing` after a media buy is live does not resend it to the seller. To change a live package, send its `pacing` in `packages[]` through [Update media buy](/v2/buyer/campaigns/tasks/update-media-buy).
## Delivery metrics
Delivery metrics roll up per package:
* `impressions`
* `spend` — gross (fee-inclusive), stated at the fee terms locked on the parent media buy, so it compares directly against the package's budget. Packages under a legacy media buy created before fee terms were locked report spend net, as the seller reported it
* `clicks`
## Meta property coverage
Meta Audience Network can deliver outside properties owned by Meta, and Meta
does not provide a complete publisher-property roster during product
discovery. Standard AdCP 3.1 Meta products therefore use an explicit, closed
set of the advertised Facebook and Instagram placements rather than allowing
Meta to expand delivery into undisclosed properties.
Eligible Meta products can retain Advantage+ Placements. Those products declare
`property_coverage.disclosure: partial`,
set `property_targeting_allowed: false`, and name the known Facebook and
Instagram properties without claiming they are exhaustive. The same trusted
contract is required again when the package is created; a candidate product ID
cannot be submitted through a legacy call. Explicit Facebook or Instagram
placement selections remain complete and support placement-level delivery
reporting.
Meta's integrated Advantage+ automation is available for website sales products
(conversion, conversion-value, and catalog) and every executable lead
destination (Instant Form, website, Messenger, Instagram Direct, WhatsApp,
and calls). Meta's current Ads Manager no longer offers manual and Advantage+
as separate setups for either family — Advantage+ is now the automation state
of one product. `ext.scope3_meta_automation` discloses `seller_optimized`
campaign-budget and bidding allocation and Advantage+ audience expansion from
the buyer's included provider-account and buyer-synced audiences. Included
audiences are optimization suggestions to Meta, not a hard delivery boundary —
Meta may expand delivery to reach people outside them. Country targeting and
custom-audience exclusions remain hard controls that Meta does not expand past.
Meta products without this automation, and all legacy or manual products, keep
audience expansion disabled by default.
A Meta package read on AdCP 3.1 carries no `budget` when Meta holds the
budget on the campaign (Advantage campaign budget) or on the ad set as a daily
budget. In that case the package's `ext.scope3_meta_budget` states where the
budget is held and any daily amount, spend cap, or minimum spend target. See
[Reading campaign budgets and daily budgets](/v2/guides/connecting-ad-platforms#reading-campaign-budgets-and-daily-budgets).
## How pacing periods create packages
Each product becomes one package per pacing period. The campaign's `pacingPeriods` defines time-windowed spend intensity within the flight, in one of two modes:
* **`weight`** — relative weights (e.g. `3.0` = 3x normal); budget is distributed proportionally across periods.
* **`budget`** — an explicit dollar amount per period.
On execute, each product is split into one package per period, with budget set proportionally (weight mode) or explicitly (budget mode). **Gaps between periods are treated as pauses** — no spend, and no package covers the gap.
Pacing periods can only be modified on `DRAFT` campaigns. After execution, the package split is locked in. See the [Pacing periods guide](/v2/guides/pacing-periods) for mode-by-mode examples.
### Example: three periods → three packages per product
```json theme={null}
{
"pacingPeriods": {
"mode": "weight",
"periods": [
{ "label": "Pre-Black Friday", "start": "2026-11-01T00:00:00Z", "end": "2026-11-26T23:59:59Z", "weight": 1.0 },
{ "label": "Black Friday / Cyber Monday", "start": "2026-11-27T00:00:00Z", "end": "2026-12-02T23:59:59Z", "weight": 3.0 },
{ "label": "Holiday push", "start": "2026-12-03T00:00:00Z", "end": "2026-12-31T23:59:59Z", "weight": 1.5 }
]
}
}
```
With this configuration, every selected product produces three packages — one per period — each weighted for proportional budget.
## Cancel a single package
To cancel one package without touching the rest of its media buy, pass `packageIds` on a `mediaBuys[]` entry in `PUT /api/v2/buyer/campaigns/:id`. Resolve the `packageId` first with [Get media buy packages](/v2/buyer/campaigns/tasks/get-media-buy-packages):
```json theme={null}
{
"mediaBuys": [
{
"action": "cancel",
"mediaBuyId": "mb_abc123",
"packageIds": ["sf_pkg_sf_mb_1783618937177_43ycbp7b_2"],
"reason": "format not supported on this device mix"
}
]
}
```
You can also update a package's budget and pacing in place by passing it in the `packages[]` array with `action: "update"`:
```json theme={null}
{
"mediaBuys": [
{
"action": "update",
"mediaBuyId": "mb_abc123",
"packages": [
{ "packageId": "sf_pkg_sf_mb_1783618937177_43ycbp7b_1", "budget": 15000, "pacing": "even" }
]
}
]
}
```
## Related concepts
Resolve a description ("the display package ending 2026-08-11") to a packageId
The parent ADCP transaction that spawns packages
Time-windowed spend intensity that multiplies products into packages
The parent media plan
# Campaign Targeting
Source: https://docs.apostra.com/v2/buyer/campaigns/targeting
Attach structured targeting intent to a campaign through the V3 MCP surface
## Overview
You can attach structured **targeting intent** to a campaign when creating or updating it through the V3 MCP surface (`save_campaign` on `/mcp/v3`). Targeting records what the campaign *intends* to reach — geography, language, device type, daypart, and demographic dimensions — and persists that intent with the campaign object.
Campaign targeting is stored and returned on the campaign workspace. When you
create a media buy with `save_media_buy`, its targeting can narrow the V3
campaign overlay but can never widen it.
## Passing targeting to save\_campaign
The `targeting` parameter is optional. Omit it to leave targeting unset; include it to attach one or more dimensions:
```json theme={null}
{
"targeting": {
"geo": [
{
"requirementId": "geo-us-only",
"strength": "required",
"include": ["US"]
}
],
"device": [
{
"requirementId": "ctv-preferred",
"strength": "preferred",
"include": ["ctv"]
}
],
"language": [
{
"requirementId": "en-only",
"strength": "required",
"include": ["en"]
}
]
}
}
```
Each dimension is an array of **requirement entries**. A requirement entry has:
| Field | Type | Description |
| - | - | - |
| `requirementId` | string | A stable, caller-assigned ID for this requirement. Use it to identify the entry in future updates. |
| `strength` | `"required"` \| `"preferred"` | Whether the dimension is a hard constraint or a preference. |
| `include` | string\[] | Values to target (must include at least one). |
| `exclude` | string\[] | Values to exclude (optional). |
## Supported dimensions
| Dimension | Values |
| - | - |
| `geo` | ISO 3166-1 alpha-2 country codes (e.g. `"US"`, `"CA"`) and DMA codes |
| `language` | BCP-47 language tags (e.g. `"en"`, `"fr"`) |
| `device` | `"ctv"`, `"mobile"`, `"desktop"` |
| `dayparts` | Day-of-week and hour-of-day slots |
| `demographics` | Demographic segment identifiers |
## Reading targeting back
After save, `targetingOverlay` appears on the campaign workspace returned by `save_campaign` and by `get(kind: "campaign")`. Existing campaigns with no targeting set omit the field entirely. The deprecated `targeting` field keeps its existing behavior: it is returned verbatim only when the campaign has stored legacy targeting.
## Updating targeting
Pass `targeting` in a `save_campaign` update to replace the stored targeting intent. Partial dimension updates replace the entire `targeting` object — each call is the full desired state, not a patch.
## Campaign targeting and media buys
`save_media_buy` applies the campaign targeting overlay as a guardrail to each
product's `targetingOverlay`. An explicit product `targetingOverlay` supplies
that buy's targeting, and campaign targeting still narrows it: overlapping
values are kept, and a product value outside the campaign returns `CONFLICT`.
A product can omit a campaign field to inherit it, or narrow it where the field
has defined containment rules. It cannot widen the campaign audience.
`signal_targeting` is excluded from the overlay because it is a priced,
product-scoped selection that will come through the signals path. Typed
`signal_targeting_groups` remains supported.
On a V3 media-buy product overlay: Deprecated. Numeric codes replace names.
Removal will be announced in release notes at least 14 days ahead; planned for
2 November 2026 (AI-10440). Codes must be in the Nielsen DMA dictionary;
unknown codes return nearby code candidates.
For country, region, metro, postal, language, browser, device type, and device
platform includes, the buy uses the overlap. Excludes are combined. A buy's
demographic age range is intersected, unknown ages are included only when both
overlays allow them, and accepted age bases are intersected. The stricter age
restriction wins. Each buy daypart must fit wholly inside a campaign daypart
on the same clock; dayparts are never clipped or converted between timezones.
For proximity, named places, and other structured values without a containment
rule, a product must omit the field or repeat the campaign value exactly.
If the included values for the same field do not overlap, `save_media_buy`
returns a `CONFLICT` error. The error names the field and includes the campaign
and product values so you can correct one of them. It does not create a media
buy that targets everywhere. Omit a product geography field to inherit the
campaign's value for that field.
Language, device, daypart, and demographic campaign intent now participate in
this guardrail. A demographic overlay still cannot dispatch while seller
transport is on AdCP 3.1, so `save_media_buy` rejects it before seller contact.
### Campaign age range at launch
A campaign's age range (`demographics.age`) is copied into every package when
the campaign launches. A media buy on a Meta storefront that approves media
buys automatically is sent under AdCP 3.2, and Meta applies the age range.
Other storefronts, including a Meta storefront that reviews media buys
manually, do not yet accept a campaign age range at launch, and the media buy
can fail there. Remove the age range from the campaign before launching on
those storefronts.
## Campaign audiences and catalogs
`save_campaign` also accepts `audienceConfig` and `catalogId`. Use
`audienceConfig.targetAudienceIds` to include up to 100 audience IDs and
`audienceConfig.suppressAudienceIds` to exclude up to 100 audience IDs. On an
update, `deleteMissing: true` replaces both lists; otherwise, supplied IDs are
added to the existing lists. `catalogId` is the public catalog ID returned by
the catalog read surfaces; pass `null` on an update to remove it.
Both fields are stored and returned by `get(kind: "campaign")`. Campaigns
with an audience or catalog cannot launch yet because those objects are not
available to sellers. The campaign readiness response names this blocker. Remove
the field to launch without it, or wait for seller object delivery support.
## Rollout and release check
This is available immediately on the public V3 `save_campaign` and
`get(kind: "campaign")` surfaces; it has no separate customer enrollment or
feature flag. The Buyer V3 API release owner verifies the release through the
automated `PR Acceptance Tests` catalog contract proof, then confirms in the
released environment that a campaign can save and read back an audience or
catalog and that a launch attempt returns the seller-sync blocker.
The release check passes only when all three behaviors agree: save persists the
requested IDs, `get` returns them, and launch stays refused while seller sync
is unavailable. Treat a missing readback value, a launch that succeeds, or a
blocker other than the seller-sync refusal as a mitigation trigger. Roll back
the release or remove the audience/catalog from the affected campaign; do not
override the blocker or treat the objects as delivered.
## Backward compatibility
`targeting` is deprecated, but its behavior is unchanged. `targetingOverlay`
is the new V3 targeting field. Its removal is a separately announced change
(AI-10440) with at least 14 days' notice. Existing calls and campaigns are not
affected.
# Bring your own creative
Source: https://docs.apostra.com/v2/buyer/creatives/bring-your-own-creative
Upload creative in chat and route it to a campaign — no separate upload screen
## Overview
Start with **Add creative** in the Creative Library when you have a finished
image or MP4 file; for a WAV or MP3 file, use **Upload** under **More** or ask
the assistant to upload it. When the upload finishes, the Library opens the appropriate
creator with the file in its first slot. Complete the creative with a name,
click-through URL, and format. From a campaign Library, this is the composer;
from an advertiser Library, it is the inline creator. Audio assets are
browse-only in the Library's own creator, so build an audio Creative through
the assistant or `save_creative`. This assembly path uses your uploaded assets
and does not require Creative Engines.
You can still hand Murph an image, video, or ad tag in a conversation. Murph
asks which campaign should receive it. HTML5 bundles also need their
landing-page URL before delivery.
This is the chat-first path for creative you already have. To generate creative from a brief instead, see [Creative](/v2/object-guides/creative) and the creative manifest tasks.
The advertiser Creative Library also has a **Catalogs** tab for checking the
feeds that can later support catalog-based creative work. Connecting or
managing a feed opens the existing catalog manager; catalog-based creative
building is not available from this tab yet.
## Previewing a creative
Select a Creative or Asset to open its previewer beside the list on wide
layouts and in a sheet on narrow ones. The list stays in place with its search,
filters, grouping, and scroll position, so close the previewer with **Escape**
or **Back to creatives** to continue browsing. The previewer shows the
available preview, status and summary facts, and attachment choices.
Promoted managed audio has an API adoption contract. See [Adopt a managed
audio source](/v2/buyer/creatives/tasks/adopt-managed-audio-source). It
records a creative and optional campaign association only; it never initiates
seller or destination delivery, attaches to a media buy, or makes anything
live.
## The flow
Attach an image, video, tag, or tag sheet in the conversation.
Murph recognizes the media kind automatically.
Murph opens a short picker showing the advertiser and that advertiser's
recent campaigns. You choose where the creative should live:
* **An existing campaign** — pick it from the list.
* **A new campaign** — describe it in plain language and Murph creates it.
* **Save to the advertiser** — park the creative at the advertiser level to
assign later. The API and agent equivalent is
[Create advertiser creative master](/v2/buyer/creatives/tasks/create-advertiser-creative-manifest).
Once the creative is on a campaign, Murph maps it to that campaign's
placements. If an asset lands in the wrong slot, you can remap it. For an
HTML5 bundle, add the advertiser destination in the creative's
click-through URL field before media-buy sync.
## Uploading to the Creative Library
On the Buyer V2 or shared V3 connector, ask to upload a creative and select its
advertiser. Both expose the same `upload_creative_asset` Task and storage flow;
no destination connection or provider account is required. Every Buyer
Account can save WAV and MP3 files up to 50,000,000 bytes as durable assets
in that advertiser's Creative Library. Accepted WAV MIME types are `audio/wav`,
`audio/x-wav`, and `audio/wave`; accepted MP3 MIME types are `audio/mpeg` and
`audio/mp3`.
WAV and MP3 upload is available to every Buyer Account: use **Upload** under
**More** in the Library, or ask the assistant to upload the file. If the
upload Task doesn't offer WAV or MP3, audio upload is temporarily unavailable
in that environment.
The Task also accepts an MP4 up to 2,000,000,000 bytes when managed-video
ingress and inspection are configured. Managed video does not depend on the
private JPEG/PNG upload option. Eligible connections accept JPEG and PNG files
up to 30,000,000 bytes.
You can select or drag up to 25 files into the upload Task at once. The Task
shows one progress list and runs no more than three private uploads at a time.
Each file can be queued, uploading, verifying, done, or failed; retrying a
failed file does not re-upload files that already finished. Before the batch
starts, enter Library tags once to apply them to every file, or edit tags for an
individual queued file. Tags are values in the built-in `tags` label dimension,
so the Library can filter them beside any other dimensions defined for the
buyer account. The same per-file limits still apply: JPEG and PNG up
to 30,000,000 bytes, MP4 up to 2,000,000,000 bytes when enabled, and WAV or
MP3 up to 50,000,000 bytes.
If a client can call tools but cannot render the embedded upload Task,
`upload_creative_asset` returns a `fallback_url`. Open that HTTPS link to
continue in Apostra with the authenticated customer and advertiser already
selected. Without campaign composition, it opens that advertiser's Creative
Library and its upload flow; no campaign is created or selected. When campaign
composition was supplied, the link instead opens the **Add creative** upload
flow directly with the campaign and any supplied creative name selected; the
user only needs to choose the local file and finish the form. The link never
contains file bytes, a signed storage capability, or provider credentials.
The browser calculates the file's SHA-256 while streaming it, then uploads the
bytes directly to private object storage with a create-only capability. 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. Every Task call rechecks the buyer, advertiser, and
exact authenticated principal.
MP4 upload preparation must finish within 30 seconds. If managed-storage
validation or secure upload setup does not finish in time, the Task returns a
retryable `SERVICE_UNAVAILABLE` error with the failed `preparePhase` and creates
no upload reservation. Reservation creation uses a bounded database commit and
then checks the durable outcome if its acknowledgement is lost. A confirmed
commit returns the upload capability, and a confirmed abort returns the same
retryable error. If the outcome cannot be confirmed, the Task returns
`COMMIT_OUTCOME_UNKNOWN`; do not retry that preparation, and contact support
with the returned trace. Every error response withholds signed upload details.
JPEG, PNG, WAV, and MP3 preparation is unchanged.
The upload capability expires after two hours. Finishing the Task verifies the
stored object's declared type and exact size, pins its immutable storage
generation, and waits for asynchronous digest and media inspection. When the
Task is saving an advertiser-owned Library asset, you can add up to 50 optional
Library tags, each no more than 64 characters. After successful finalization,
Task activity on a first-party host gives the assistant the durable asset ID,
saved name, tags, content type, byte size, SHA-256 digest, and available verified
media facts. Video facts include container, codecs, dimensions, and duration.
Audio facts include container, codec, bitrate, channel count, sample rate, and
duration. On Claude and ChatGPT, the follow-up gives the assistant only the formatted size,
verified media kind, advertiser name when present, every Library asset ID, and Creative Library
link. The uploaded file name, tags, SHA-256 digest, verified media facts, and authorisation guard stay in
the Task disclosure. Signed upload URLs, private storage paths, publication URLs,
video frame rate, and provider identity remain excluded.
After an advertiser-scoped Library upload succeeds, the confirmation card shows
the asset preview, its name, media kind, byte size, and the Library tags that
were saved. For MP4 files, inspection chooses a representative still from the
middle of the video. It avoids black, flat, and blurry samples where possible,
and uses a lower-confidence fallback when every sample is unsuitable. The
Library, receipt, and preview show that still after inspection completes; until then,
they show the video icon or play glyph. For a video Creative, the image in the
thumbnail slot is used wherever the Creative shows a still; otherwise it uses the
inspected poster frame.
The upload receipt shows the poster once it is ready. Sellers receive the poster
only where their destination has a thumbnail slot. After the upload finishes, the Task sends
one plain sentence with a link to the Creative Library; the raw upload receipt remains behind
an **untrusted receipt** disclosure. Select **See it in your Library** to open the Assets lens
on that exact asset, or **Upload another** to start again. Hosts that cannot
render the card receive the same plain confirmation and an authenticated Apostra
chat link to the Library.
The assistant can find promoted audio with `search(kind: "creative_asset")` by
passing the advertiser ID in `filter.advertiserId`. To read one with `get`, pass
the asset ID as `id` and the same advertiser ID as `sourceId`. Every read
rechecks the customer, principal, and advertiser; the asset ID grants no
authority. You can save an advertiser-scoped draft with
`formatKind: "audio_hosted"`, its URL-free canonical `formatParams`, and the
uploaded asset in `sourceAssets`. Finalization does not create or attach a
Creative. Product fit is checked later when you attach the draft or use it in a
media buy. Existing provider-managed audio adoption is unchanged. Rejected or
abandoned quarantine objects never become public delivery assets.
Upload finalization is not campaign association or destination delivery.
Assemble a promoted image, MP4, or inspected audio asset through
`save_creative`. Before choosing a product, name its URL-free canonical
`formatKind` and `formatParams`; do not substitute a legacy format reference.
Managed MP4 upload is generally available to authenticated Buyer Accounts where
its upload and inspection services are configured. WAV and MP3 upload is
available to every Buyer Account through **Upload** under **More** in the
Library or the assistant's upload Task. If the upload Task doesn't offer WAV
or MP3, audio upload is temporarily unavailable in that environment. JPEG and
PNG eligibility remains independent.
## Add creative in the Creative Library
Open the advertiser or campaign **Creative Library** and select **Add creative**.
The upload flow opens inside the Library. For images and MP4 video, it opens the
appropriate creator with the file in its first slot. Audio assets are browse-only
in the Library's own creator, so build an audio Creative through the assistant
or `save_creative`. Use **More** for **Upload** when you only want to add a
reusable file, including WAV or MP3, or **New creative** when you want to
start from a file already in the Library. You can also upload WAV or MP3 by
asking the assistant.
An authorized buyer can also assemble an advertiser-owned Creative before a
campaign exists. A finished upload keeps its original filename as its display
name.
To save a draft, either name a canonical format such as image, hosted video, or
hosted audio, or pick a product to use its seller format.
The **Assets** tab contains reusable files uploaded directly to the Library.
Files inside existing Creatives are not listed there. The **Catalogs** tab lists
connected catalog feeds; it is separate from Creative collections.
Files already inside existing Creatives stay with those Creatives and do not
appear in **Assets**. A file you upload stays private to you until you use it
in a Creative.
In the Library's **New creative** flow, select an uploaded file first. The
Product picker defaults to **Compatible with this asset**, with **All products**
and search by product or seller available when you need to compare options.
Each row names the seller and canonical format, and lists the hard requirements
that matter for the file: media type, hosted-file or VAST/VPAID delivery, file
type, dimensions, duration, container or codec, and file-size limit where the
seller declares one. A row explains why the selected file fits or does not fit.
Selecting a product collapses the list and moves focus to its Format and slots
step; choose **Change product** to return to the list. If no product accepts the
file, choose **Upload another asset** to return to the Library uploader, or
**View all products** to inspect every available product and its constraints.
The Library reads each product's buyer-visible canonical format options and
verifies the selected option again before showing its slots. Select a compatible
asset and place it with **Place selected** or drag it into the slot. **Done**
makes one advertiser-scoped `save_creative` call and shows the authoritative
Creative readback. You can select **Show preview** there to load the exact saved
revision, then return to **All creatives** to find the Creative once. It does
not attach the Creative to a campaign or contact a provider.
Name the format in one of two ways. Before choosing a product, select the
canonical format family and its parameters, such as `image` with a size or
`video_hosted` with a duration. If you have already chosen a product, enter its
exact opaque Product ID, including the `sf2:` prefix, and the Library resolves
that product's buyer-visible format option and slots. Select a compatible asset
and place it with **Place selected** or drag it into the slot. **Done** makes
one advertiser-scoped `save_creative` call and shows the authoritative Creative
readback. It does not attach the Creative to a campaign or contact a provider.
When you later attach the Creative or use it in a media buy, Apostra checks
that its format kind fits the selected product. It does not yet compare format
parameters such as image size or duration at that step.
The Library checks file type and size from the durable asset record. For a
promoted MP4, it also checks the verified container, video and audio codecs,
exact or ranged duration, width, and height before enabling placement. A
mismatch or missing inspection fact is rejected before save. Formats that
require text, audio, HTML, nested groups, aspect ratio, or another unsupported
property remain visible with the reason they cannot be saved there. Use the
agent workflow for those formats and supply every required canonical slot
explicitly. The Library never falls back to the legacy global format catalogue.
## Compose a campaign draft
Choose **Compose** in a campaign's Creative Library to open the Creative
composer Task. Pick uploaded assets, fill the format's slots, set the draft
name and click-through URL, then select **Save creative**. The preview updates
while you work, and the save keeps the draft in that campaign's Creative
Session. This assembly flow is available whether or not Creative Engines is
enabled.
Ask for copy or other changes in the host chat. The composer has no separate
change bar, so it reloads the session after chat updates it. A seller's
rejection or requested change can open the composer with that note pinned.
Saving this draft does not create delivery, attach it to a campaign, or make it
live. Use the existing **Attach to campaign** picker from the saved Creative's
**Used in** section when the draft is ready.
Agents on the shared V3 connector can open the same Task with
`open_creative_library` using `lens: "composer"` and the campaign ID. A
composer launch always requires `campaignId`; add `sessionId` only to resume a
specific editable Creative Session. This lens is buyer-only; seller accounts do
not receive it. Without a session ID, Apostra creates a durable assembly
draft with the campaign's single supported composer format. For an entitled
Buyer Account with a connected engine, the draft records that connection;
otherwise it has no engine binding. It does not call a Creative Engine.
If the campaign has no single supported composer format, the draft starts as a
Snap Story and the Task says to change the format in chat. Hosts that do not
render MCP apps receive an Apostra chat link with `creativeAction=compose`,
the campaign ID, and the session ID when one was selected.
The composer leaves asset assembly and save controls available when generation
is unavailable. If Creative Engines is off, it tells you to ask your Apostra
account team to enable it for the Buyer Account. If Creative Engines is enabled
but this draft has no connected engine, it tells you that generation needs a
connected Creative Engine on the draft. Generation also needs any applicable
provider setup described in [Creative Engines](/v2/setup/v3/creative-engines).
## Turning an uploaded image into a creative
Pass the finalized JPEG or PNG `scope3-asset://` reference verbatim as
`sourceAssetRef` to `save_creative`, plus the matching `advertiserId` or
`campaignId`. Campaign-scoped adoption derives the owning advertiser from the
campaign.
For an eligible reference:
```json theme={null}
{
"campaignId": "cmp_123",
"name": "Loetje image hero",
"sourceAssetRef": "scope3-asset://v1/123e4567-e89b-42d3-a456-426614174000"
}
```
Use either `campaignId` or `advertiserId`, not both. The upload must be
finalized and owned by the same customer, authenticated principal, and
advertiser. The server copies the reverified JPEG or PNG bytes into the
governed Creative asset store and persists only that durable asset plus a
one-way source fingerprint. It never stores the opaque source reference,
private object path, or upload/delivery signed URL on the Creative.
For a campaign, creation happens in the advertiser library first and the
existing campaign-membership operation runs second. Retrying the same source
and name returns the Creative already created and completes a missing campaign
attachment, without replaying campaign-scoped create side effects. The private
upload can then expire without emptying the Creative.
## Google Ad Manager image destinations
Before launching a campaign that routes an image creative to Google Ad Manager,
check whether the route requires a click-through destination. When it does, call
`save_creative` with the creative's `clickUrl` before launch. `save_campaign`
refuses the launch if that required destination is missing. Save the creative
with a valid destination URL, then launch the campaign again.
The requirement applies only to routes that require a destination URL. A
media-buy creative readback includes `clickUrl`, so an agent can confirm the
saved destination before it launches.
## Turning an uploaded media asset into a creative
Call `save_creative` with either the advertiser or a campaign and one
`sourceAssets` binding for every required slot. There are two ways to name the
format: use a signed, product-bound `creativeFormatId` when a product has
already been selected, or, for an advertiser-scoped Creative before choosing a
product, use the URL-free canonical `formatKind` and optional `formatParams`.
The
second path has no product binding. When you later attach the Creative or use
it in a media buy, Apostra checks the canonical format kind and parameters
against each product. The result is per product: the Creative either fits,
fits with named adaptations (`resize`, `letterbox`, `trim`, or `transcode`),
cannot be confirmed because facts are missing, or cannot fit. A Creative without
the required size, duration, codec, or container in `formatParams` needs an
inspected uploaded asset that establishes that fact before Apostra can
confirm its fit. A failed fit names the closest declared option and the difference,
such as a 3-second audio file against a 15–30-second requirement. An attachment
declares the required adaptations but does not perform them. Supply required copy
and destination slots through `social` and `clickUrl`. For a composition that uses
`sourceAssets`, ordinary `assets` are unbound and cannot satisfy the selected
format's required slots.
## Format parameters
For an advertiser-scoped Creative that names `formatKind` directly, pass only the
URL-free `formatParams` keys listed for that kind. An unknown key is rejected.
The list comes from the same format constraints that validate the save.
* `image`: `width`, `height`, `sizes`, `min_width`, `max_width`, `min_height`, `max_height`, `aspect_ratio`, `max_file_size_kb`, `image_formats`, `ssl_required`
* `html5`: `width`, `height`, `sizes`, `min_width`, `max_width`, `min_height`, `max_height`, `max_initial_load_kb`, `max_polite_load_kb`, `host_initiated_subload`, `max_animation_duration_ms`, `max_cpu_load_percent`, `mraid_required`, `mraid_version`, `om_sdk_required`, `clicktag_macro`, `backup_image_required`, `backup_image_max_size_kb`, `ssl_required`
* `display_tag`: `width`, `height`, `sizes`, `min_width`, `max_width`, `min_height`, `max_height`, `supported_tag_types`, `max_redirect_depth`, `backup_image_required`, `om_sdk_required`, `ssl_required`
* `image_carousel`: `min_cards`, `max_cards`, `card_aspect_ratio`, `allowed_card_media_asset_types`, `card_image_max_file_size_kb`, `card_video_max_duration_ms`, `ssl_required`
* `video_hosted`: `duration_ms_exact`, `duration_ms_range`, `aspect_ratio`, `orientation`, `min_width`, `max_width`, `min_height`, `max_height`, `video_codecs`, `audio_codecs`, `containers`, `max_bitrate_kbps`, `max_file_size_mb`, `captions`, `om_sdk_required`
* `video_vast`: `duration_ms_exact`, `duration_ms_range`, `aspect_ratio`, `orientation`, `vast_version`, `vpaid_enabled`, `vpaid_version`, `simid_supported`, `linear_required`, `skippable_after_ms`, `max_wrapper_depth`, `ssl_required`
* `audio_hosted`: `duration_ms_exact`, `duration_ms_range`, `audio_codecs`, `audio_sample_rates`, `audio_channels`, `max_bitrate_kbps`, `loudness_lufs`, `companion_image_required`
* `audio_vast`: `vast_versions`, `vast_version`, `media_file_requirements`, `duration_ms_exact`, `duration_ms_range`, `skippable_after_ms`, `max_wrapper_depth`, `ssl_required`, `companion_image_required`
* `audio_daast`: `duration_ms_exact`, `duration_ms_range`, `daast_version`, `linear_required`, `max_wrapper_depth`, `companion_image_required`, `ssl_required`
* `sponsored_placement`: `supported_catalog_types`, `min_items`, `max_items`, `fanout_mode`, `required_catalog_fields`, `supported_id_types`, `hero_asset_supported`
* `native_in_feed`: `title_max_chars`, `body_text_max_chars`, `cta_max_chars`, `cta_values`, `main_image_sizes`, `icon_size`, `max_image_file_size_kb`, `image_formats`, `ssl_required`
* `responsive_creative`: `headlines_min`, `headlines_max`, `headline_max_chars`, `long_headlines_min`, `long_headlines_max`, `descriptions_min`, `descriptions_max`
* `agent_placement`: `platform_extensions`
* `coordinated_placements`: `platform_extensions`
* `seller_rendered_stateful_display`: `platform_extensions`
* `custom`: `format_shape`
For a repeatable group, bind every member with the same zero-based instance:
`cards[0].media`, `cards[0].headline`, then `cards[1].media`,
`cards[1].headline`. Put uploaded image, video, or audio members in
`sourceAssets[].slot` and text or URL members in `social.components[].slot`.
Every required member must be present for each instance, and indices must be
contiguous from zero.
Meta-style hosted video with a required thumbnail:
```json theme={null}
{
"advertiserId": "900",
"name": "Loetje video",
"formatKind": "video_hosted",
"formatOptionRef": {
"scope": "product",
"format_option_id": "meta-feed-video"
},
"sourceAssets": [
{
"assetRef": "scope3-video-source://v1/123e4567-e89b-42d3-a456-426614174000",
"slot": "video",
"makePrimary": true
},
{
"assetRef": "scope3-asset://v1/223e4567-e89b-42d3-a456-426614174000",
"slot": "thumbnail"
}
]
}
```
Use an exact `formatOptionRef` from the buyer-visible product catalogue when a
product is already selected. Otherwise name the canonical `formatKind` and
its parameters directly; this does not invent a product or a legacy format
reference. The
MP4 must be promoted, every source must belong to the same customer,
authenticated principal, and advertiser, and every required slot must be
present. With `advertiserId`, the save writes an advertiser-owned Creative
without attaching it. To attach that existing Creative later, open it in the
Creative Library from the advertiser view. Its **Used in** section lists the
advertiser's draft, active, and paused campaigns and marks the ones that can
take it. A campaign can take the Creative when at least one of its sellers
accepts the Creative's format. Campaigns whose flight has ended, campaigns with
no sellers yet, and campaigns whose sellers could not be checked are listed but
cannot be selected. In chat, asking to put a Library creative on a campaign sets
the same membership through `save_creative` with `campaignIds`, or through
`save_campaign` with `creativeIds`; the V2 `attach_creatives_to_campaign`
operation performs the same attachment outside the Library. With `campaignId`, `save_creative`
additionally requires the option on that campaign's products and attaches the
Creative. A Library MP4 remains
`destination_required` until the governed V2
`attach_creatives_to_campaign` operation finds a compatible package whose
selected destination is Meta. At that point Apostra rechecks the current
buyer, advertiser, Meta account authorization, Library asset, and source
revocation before Meta receives the existing MP4. Other destinations remain
unsupported for this Library bridge and require no second upload.
The same uploaded source can be used in multiple named Creatives while it is
retained. Retrying the same name and exact composition returns the existing
Creative. Images are copied into governed Creative storage. A hosted MP4
capability expires 90 days after source approval. Meta delivery renews that
capability from the durable Library asset only while the exact source is still
active and has not been revoked. Private source references, private storage
URLs, and signed upload capabilities are never returned as provider input.
## Legacy provider-bound video adoption
An MP4 finalized by the shared V3 buyer Task is owner-bound and must use the V3
composition path above. The legacy V2 operation below is not a fallback for it.
A direct Storefront connection offers the legacy provider-bound upload Task
only when the host's upload origin is one Apostra has registered for private
storage. ChatGPT's Storefront app uses one fixed, registered origin, so it gets
the Task. Claude derives a separate origin from each Storefront's connector
URL, so a Claude direct Storefront connection usually does not get the Task.
If the Task isn't offered, upload through the Buyer V2 or shared V3 connector
instead, as described in
[Uploading to the Creative Library](#uploading-to-the-creative-library).
For a video uploaded through the legacy provider-bound Task, finishing returns
the `source_uid` for the source that passed inspection. That same provider
scope can adopt it without assembling a creative around a hosted URL by hand:
```http theme={null}
POST /api/v2/buyer/creatives/adopt-managed-video-source
```
```json theme={null}
{
"source_uid": "0f8b7a2e-6cf4-4db4-9a7f-09db4430ae91",
"advertiser_id": "900",
"storefront_id": "7",
"name": "Loetje hero cut",
"campaign_id": "cmp_123"
}
```
`campaign_id` is optional. Include it and the creative is attached to that
campaign in the same call; omit it to keep the creative advertiser-scoped. The
campaign and source must match the same advertiser and provider ownership.
Attaching also pushes the campaign's live media buys to their sellers. That push
is best-effort and is not part of what the response confirms: a success means the
creative is attached to the campaign, not that every seller has accepted it into
its packages. Check the media buys themselves to see where the creative is live.
The V2 response returns `creative_id`, `source_uid`, `delivery_url`,
`publication_uid`, `name`, and `created`. A success that named a `campaign_id`
means the creative is attached to that campaign whether or not this particular
call attached it, so a retry safely finishes an interrupted attachment.
**You name the source, not a URL.** The hosted HTTPS URL is issued server-side
and resolves the exact file that passed inspection, so the private storage
location is never something you supply or can substitute.
**Adopting the same source twice returns the creative that already exists**,
with `created: false`, rather than making a second one. If you include a
`campaign_id` the second time, the existing creative is attached to that
campaign — so a call that was interrupted after the creative was made can be
retried safely. Retry with the same `name`: adoption never renames an existing
creative, so asking for a different one is refused rather than silently
returning a creative that does not have the name you asked for.
Names compete among an advertiser's creatives that are not currently scoped to a
campaign — which is how adoption always creates them. If one of those already
holds the name, adoption is refused rather than silently reusing it. A creative
created directly under a campaign is outside that set while it stays there, but
detaching it moves it in: detaching does not free a name, it claims one. Attaching an adopted creative to
a campaign afterwards does not release its name either: archiving it does, and so
does renaming it with a separate `update_creative` call. If the creative holding
the name is already attached to a campaign, rename it instead — renaming frees the
name without disrupting a creative that's live in a media buy. `update_creative` is campaign-scoped, so a creative attached to no
campaign can only be archived. Note that archiving is the advertiser-scoped
delete (`archive_creative`); the campaign-scoped `delete_creative` only detaches
the creative from that campaign and does not release its name. Duplicating an
adopted creative with `duplicate_creative` copies the video, not the adoption:
the copy is not the adoption of record for that source, so it neither blocks nor
lets you re-adopt the source through it.
A `CONFLICT` on `name` does not always mean "pick another name". Each one
carries a `details.reason`: `name_creation_in_progress` means another call is
still writing that exact name, and retrying the identical request is the right
move; `name_creation_abandoned` means that writer died mid-way, so archive the
incomplete creative first; `name_taken` means the name belongs to a finished
creative and you need a different one. The conflicts on `source_uid` work the
same way — `adoption_in_progress` is the only one worth retrying.
`adoption_name_mismatch` means the source already has a creative under another
name; use that name or rename the creative separately. `source_delivery_revoked`
and `source_deletion_requested` are terminal for that source: upload a new video
before adopting again. Branch on `details.reason` rather than on the message text.
Two rules here are enforced by the database, not just checked: a managed video
source can back only one live creative, and advertiser-scoped creative names are
serialized across both adoption and ordinary creation. Both hold no matter how
many callers race, and archiving frees the name.
The `delivery_url` is not permanent. It stays valid while the source is
retained and its delivery has not been revoked. Once the retention window
ends, or once the delivery has been revoked with nothing live replacing it,
re-adopting that source is refused — with a message telling you what
happened, not a name collision. Requesting deletion of the source refuses it
too, by taking the source out of the adoptable state. None of these can be
refreshed in place: upload a new video and adopt that new source, usually
under a new name, since the existing creative still holds its old video until
you archive or rename it. A deletion request also stops a call that was only
trying to finish an interrupted attachment — a source you have asked us to
erase is not attached to anything further.
If the source is already held by a creative but adoption can no longer confirm
that creative still carries the video — because its asset was replaced or
removed directly on that creative — adoption refuses with `CONFLICT` rather than
creating a second creative for the same source. Archive the creative holding it
and adopt again, or upload the video again and adopt the new source.
A second `CONFLICT` also tells you to archive, for a different reason: an earlier
adoption of this same source left its creation marker uncleared and has been
stuck long enough (about fifteen minutes) that nothing is still writing it. An
asset row may already have landed, but the creative never finalized. Its
`details.reason` is `adoption_abandoned`; archive that incomplete creative and
adopt again.
Within these three source-identity conflicts, `adoption_in_progress` is the one
you should simply retry. It means another adoption is still finishing or the
blocking adoption changed while this request was classifying it; retrying lets
the request observe the settled state. The separate
`name_creation_in_progress` reason described above is also retryable. Branch on
`details.reason`, never on the message; the other reasons require archiving or
changing the request first.
Legacy adoption produces a creative and may enqueue existing V2 campaign sync
behavior; its success response proves neither provider creation nor acceptance.
Only exact provider readback confirms destination materialization. For the
managed-audio contract, see [Adopt a managed audio
source](/v2/buyer/creatives/tasks/adopt-managed-audio-source).
Adoption is available wherever direct video delivery is configured — it is part
of the generally available managed-video capability, not a separate account
opt-in. A request made while the delivery service is unavailable is refused
with `CAPABILITY_NOT_SUPPORTED`.
## Uploading HTML5 banner ZIPs
Some ad tools export one HTML5 banner as a ZIP containing `index.html` and its
scripts, styles, images, fonts, or video. The V2 buyer API keeps a
recognized package together as one HTML5 creative.
The entry point may be `index.html` at the ZIP root or, for a single-folder
export, `banner/index.html`. All creative files must be inside that one folder;
multiple entry points or multiple enclosing folders are rejected. Creative file
contents and relative asset paths are preserved, and the entry point is recorded
for delivery. macOS metadata (`__MACOSX`, `.DS_Store`) and hidden directories
such as `.vscode/` are ignored when identifying creatives. Retained files in hidden
directories must still use supported file types. `.DS_Store` and AppleDouble
resource-fork entries (`._*` under `__MACOSX/`) are removed from the delivered ZIP,
so their names cannot exempt retained content from the supported-type check.
ZIPs without those metadata entries keep their original bytes. SVG and MP4 files
are supported inside the bundle. All input metadata counts toward file-count
and decompressed-size limits before removal. The campaign file-upload UI also
ignores hidden directories when expanding files; ordinary non-hidden
subdirectories remain available.
A retained HTML5 bundle may contain at most 100 files. Both its ZIP bytes and
its total decompressed contents must fit within 10 MiB, and its entry HTML
must fit within 1 MiB. The V2 backend allows at most 200 total entries in each
ZIP, including folders. Metadata counts toward these limits even when it is
ignored for creative recognition or removed before delivery.
The entry HTML must declare exactly one unambiguous size:
```html theme={null}
```
Replace `300` and `250` with the banner's actual positive integer pixel
dimensions. Dimensions are not inferred from the ZIP filename, CSS, images,
video, or a separately supplied landing URL. Missing, invalid, or conflicting
`ad.size` declarations produce that specific requirement. A readable ZIP that
fails the creative contract is reported as an import rejection, not as a corrupt
or unreadable archive. Traversal paths, duplicate paths, symbolic links,
unsupported files, ambiguous entry points, and oversized bundles remain blocked.
V2 also recognizes wrapper exports containing one nested HTML5 ZIP, an exact
exploded copy, and an explicitly named same-size backup image. The nested package
keeps its creative files and relative paths. Its removable macOS metadata is
stripped using the same cleanup described above, so its ZIP container bytes may
change. In V2 Murph, an attachment used on a later turn is retrieved
from the account's saved attachment pool before inspection. If those bytes cannot
be retrieved, the error says the attachment was unavailable and that its archive
contents were not inspected. Inline files and saved attachments share the
existing 50 MiB total chat upload budget when V2 Murph retrieves a batch. A batch
that cannot fit returns that limit explicitly before creative inspection; use
fewer or smaller files in the call.
**Claude/Apostra connector availability:** attaching a ZIP to a Claude
conversation does not itself upload it to Apostra. The V3 `save_creative`
surface and managed-media upload Task do not currently adopt HTML5 ZIPs. Use the
existing V2 buyer upload flow for HTML5 ingestion; a connector with only V3 tools
cannot perform that upload. An unavailable upload path is not evidence of archive
corruption. Support should identify the connector surface and the actual upload
result before diagnosing the file.
Importing and delivering are separate checks. Before media-buy sync, an HTML5
creative needs an explicit landing-page URL in its click-through URL field. A
placeholder clickTag or missing destination is a delivery concern, not an archive
integrity failure. A seller option whose HTML5 behavior requirements cannot be
proved from inspection is rejected with the blocking requirement named.
## Uploading tag sheets
Murph accepts creative manifests in CSV, legacy XLS, and XLSX. CM360/DCM tag
sheets can include instructions before the data table and several alternative
tag implementations for one placement-tag row; Murph finds the actual header
and creates one creative per populated display row rather than attaching every
display alternative as a duplicate.
For display, Murph prefers the iframe/JavaScript implementation and uses
JavaScript or Standard as fallbacks. It retains every populated display, VAST
pre-fetch, and Internal Redirect alternative as inert source provenance. Inline
third-party markup is stored as a non-trafficable custom creative without
rewriting vendor macros. Murph does not mislabel it as canonical `display_tag`
(which requires a hosted tag URL) or as an HTML5 bundle. A sheet containing only destination-sensitive alternatives is
recognized but not converted into an arbitrary primary creative. VAST version
must match the destination player, while Internal Redirect requires separate
image and click URL fields in a compatible ad server. When modern and legacy
sheets contain the same placement name and size, the modern row takes
precedence; unmatched legacy rows are retained.
Declared dimensions such as `300x250` are retained for destination compilation.
Murph can also read dimensions from common tag markup, including HTML
attributes, CM360 inline styles, and `sz=` parameters. A dimension declared in
the sheet takes precedence over a size inferred from markup.
### Preview and improve the mapping
Use the [Inspect a Tag Sheet skill](https://api.apostra.com/skills/inspect-tag-sheet/SKILL.md)
to preview a CSV, XLS, or XLSX file locally before upload. Its offline inspector
returns the parser version, mapping-profile version, mode, creative-row count,
canonical role and rule IDs, and diagnostic codes. The receipt contains no
filename, worksheet name, source header, placement name, tag, URL, formula, or
cell value.
The mapping profile is versioned. When the production parser can read a
workbook but finds no usable rows, Apostra automatically records the same
content-free receipt. When that shape recurs across customers, it sends one
deduplicated, content-free signal for team review. Corrupt files and size-limit failures are recorded for diagnosis but do
not automatically become product work. Destination-sensitive rows are
recognized and require a compatible destination before trafficking. A team
member or authorized agent reviews the signal, reproduces it with synthetic or
explicitly approved sanitized data, adds a regression fixture, and publishes a
new profile version.
If local inspection finds a plausible but incorrect result, such as the wrong
row count, the skill can prepare the same sanitized receipt for support. It asks
for confirmation before filing and never sends the workbook or its contents.
Customer workbooks do not enter the shared test corpus by default.
## Uploading before you've named an advertiser
You don't have to know the advertiser first. If you drop a file at the start of
a conversation, Murph **stashes** it and holds onto it across turns. The moment
you name (or pick) an advertiser, the stashed upload is adopted into the
"which campaign?" picker — nothing is lost between the upload and the decision.
## What happens to your creative
* The asset is stored against your advertiser and, once routed, attached to the
chosen campaign.
* A creative saved to the advertiser (rather than a campaign) waits there until
you assign it — it is not sent to any seller until it's on a live campaign.
* Attaching to a campaign makes the creative eligible to enter delivery; it
must still clear any [storefront and source-system approval gates](/v2/buyer/creatives/approval)
before it can serve. Saving to the advertiser or stashing does not begin that
process.
## Related
* [Creative](/v2/object-guides/creative) — the creative manifest model
* [Creative tasks](/v2/buyer/creatives/tasks/index) — manifest task reference
# Generative creative provider credentials
Source: https://docs.apostra.com/v2/buyer/creatives/generative-creative
How creative provider accounts and model keys are connected, mapped, and rotated
Organizations with generative creative enabled can connect provider API keys
for OpenAI, Gemini, and fal.ai. The key determines whose provider account pays
the model cost; it does not change which Apostra organization owns the
campaign or advertiser.
AudioStack and ElevenLabs credentials are managed under **Settings →
Connections → Creative tools**. In **Advertiser mapping**, map the AudioStack
or ElevenLabs access grant to each advertiser. The mapping determines which
provider tenant is billed. Generation for an advertiser without a
mapping fails before it starts; Apostra never falls back to another
provider account.
Replacing an AudioStack or ElevenLabs key creates a new access-grant identity.
Confirm its advertiser mappings again before generation resumes. This prevents
a key for a different provider tenant from inheriting the previous tenant's
mappings.
## How an OpenAI, Gemini, or fal.ai key is selected
Provider keys belong to the organization and have a label. You can assign one
key to several advertisers or mark one key as the organization default for that
provider.
When generation starts, an advertiser-assigned key takes precedence. If the
advertiser has no assigned key, Apostra uses the provider default only when
you explicitly set one. It never silently chooses another key, because that
could charge the wrong provider account.
## Rotate a key
A delegated call that selects a specific connection credential fails if that
credential becomes unavailable. It cannot substitute a local development
environment key for the selected credential.
In **Settings → Generative creative**, open the key's action menu and choose
**Rotate key**. Enter the replacement API key and confirm.
Rotation preserves the credential's label, advertiser assignments, and default
status. The existing key remains active unless the replacement is stored and
the credential is switched successfully. After the switch, Apostra retires
the old stored secret.
You can also rotate through the buyer API:
```http theme={null}
PATCH /api/v2/buyer/creative/model-credentials/{credentialId}
Content-Type: application/json
{ "apiKey": "your-replacement-provider-key" }
```
Provider keys are stored encrypted and are never returned by the API. List
responses include only a masked hint.
## Provider costs and Intelligence Units
Creative generated with your provider key is charged by that provider to your
provider account. Buyer-funded provider attempts are excluded from
Apostra's generated-media IU meter, including retries and discarded
variants.
AudioStack and ElevenLabs remain bring-your-own-key regardless of the
organization's other billing settings. Their credentials are managed on the
provider connection, not in **Settings → Generative creative**.
The mapped provider account controls billing; voice selection is independent.
Set the brand's durable default voice with `brand.voice_synthesis`, or set
transformer `voice_id` to override it for one build.
## Provide exact copy for ElevenLabs voice drafts
ElevenLabs speaks only explicit voice copy. Put the exact words to speak in
straight or curly quotation marks in `creative_brief.prompt`, or provide a
`creative_manifest.assets.script` text asset. For example:
```json theme={null}
{
"creative_brief": {
"prompt": "Friendly 15-second climbing-gym spot. \"Chalk up. Climb higher. Your first class is on us this week.\""
}
}
```
Direction without quoted spoken copy, such as a requested tone or duration,
does not give ElevenLabs words to say. The request stops before the provider is
called and asks you to add a script. An assistant preparing a voice draft should
write a short script in quotes from the buyer's confirmed intent, or ask the
buyer to provide the exact copy first.
### Clip length follows your script
The clip is as long as it takes to speak your words, not as long as the slot you
name. A 15-word line produces a clip of about six seconds whether the slot is 15
or 30 seconds, and you can add music or silence to fill the slot later. A
duration in the brief, such as "15-second", is a target and never causes a
rejection.
Apostra checks the generated audio against your script. It estimates the spoken
length and rejects audio that is under one second or under 40% of that
estimate, because such audio cannot be your script. For space-separated
languages the estimate is about 160 words per minute. Chinese and Japanese
scripts have no spaces between words, so they are measured at about five
characters per second, and Thai at about two words per second. If Apostra
cannot estimate the length of your script, it cannot show the clip is long
enough and rejects it as `generated_audio_too_short`. Audio that cannot be
decoded is also rejected. When a leaf is rejected after the
provider was called, the generation result carries a `failureReason` on that
leaf, one of `generated_audio_too_short`, `generated_audio_undecodable`, or
`provider_error`. The provider may have charged for that attempt, so the leaf is
not retried automatically; create a new generation action to try again.
# Publish a managed rendition as VAST
Source: https://docs.apostra.com/v2/buyer/creatives/rendition-vast-publication
Turn a ready destination rendition into one stable, revocable VAST 4.2 URL
The managed rendition VAST canary turns a ready, immutable video rendition into
one HTTPS VAST 4.2 URL. An ad server fetches that URL and receives the exact
rendition set approved for the destination, with one time-bounded media URL per
file.
This is an enrolled-account canary, not general availability or destination
certification. Apostra provisions the ready rendition job for each canary.
Uploading an MP4 does not create a public VAST URL by itself.
## When to use it
Use a managed publication when the destination needs VAST rather than a raw
hosted MP4. The publication step does not make an incompatible file compatible:
the source must first pass inspection and produce a ready rendition for the
destination profile.
The current certification boundary is unchanged:
* Google Ad Manager is the first end-to-end canary destination.
* Managed FreeWheel ingestion is not certified yet.
* A seller policy can narrow a certified destination profile, but cannot widen
an unknown platform limit.
## Publication flow
Apostra prepares the canary job against the selected storefront and
destination profile. The job must be ready, and its source must remain
approved or promoted.
Call `publish_rendition_vast` with the advertiser, job, storefront,
publication expiry, and the approved HTTPS impression-tracking contract.
Supply a stable idempotency key and reuse it only for an identical retry.
The response contains `vastUrl`, an HTTPS URL suitable for the destination
canary. The VAST document and its media set do not change in place.
Call `revoke_rendition_vast` to disable the VAST URL and every embedded
media capability together. To replace a live publication, publish a new
immutable version with `rotatePublicationUid` and a new idempotency key.
Generated VAST includes five progress trackers next to the approved impression
tracker: start, first quartile, midpoint, third quartile, and complete. They
are included only when the approved impression URL uses Interchange's tracking
receiver, so third-party impression contracts stay unchanged. VAST players fill
`[CACHEBUSTING]` and `[TIMESTAMP]` when they fire the progress trackers. The
approved impression URL is never altered.
## Delivery and security guarantees
* The public URL reveals no bucket, object name, or storage credential.
* Publication, tracking, and media authority all expire; the shortest valid
authority is the effective delivery lifetime.
* Revoked, expired, incomplete, or no-longer-ready publications return `404`
without disclosing which check failed.
* VAST responses are digest-verified and served with `no-store` caching.
* Publication and revocation require buyer write authority and are scoped to
the authenticated customer, advertiser, storefront, and rendition job.
## Pricing boundary
Raw upload remains included. This canary does not add an upload or publication
charge. New Rate Cards list successful managed rendition output as a Measuring
activity, counted only after every required output passes inspection. That row
has no price and cannot consume IUs. Pricing the output, retained storage,
optimization, or delivery would require a separate commercial decision and a
successor Rate Card before any of them could appear on a bill.
## Related
* [How AdCP thinks about video](/v2/concepts/video-creatives)
* [Bring your own creative](/v2/buyer/creatives/bring-your-own-creative)
# Get products across storefronts
Source: https://docs.apostra.com/v2/buyer/discovery/multi-storefront-get-products
Use one canonical AdCP-shaped request to retrieve, evaluate, refine, and buy proposals across your connected storefronts
`get_products` is the canonical product door for buyer agents. Send one
AdCP-shaped request to selected storefronts—or all storefronts connected to an
advertiser—and receive one bounded canonical page as `products[]` plus optional
sibling `proposals[]`. Every identity remains qualified by its originating
storefront.
Apostra can optionally evaluate valid seller proposals before returning
them. You supply plain-language operating instructions. A managed model chooses
`accept`, `reject`, or `refine`, can attach buyer-owned enrichment, and can rank
the accepted cohort across sellers.
AdCP remains a one-buyer-to-one-seller protocol. Apostra is the
many-to-one buyer layer: it fans out the canonical request, validates each
seller independently, qualifies identities, and returns a bounded aggregate
page. Sellers do not see one another or receive an Apostra-specific
request.
## One protocol request, marketplace scale
```mermaid theme={null}
flowchart LR
B[Buyer agent] -->|canonical request + ext.interchange| I[Apostra get_products]
I -->|1:1 AdCP get_products| S1[Storefront A]
I -->|1:1 AdCP get_products| S2[Storefront B]
I -->|1:1 AdCP get_products| SN[Storefront N]
S1 --> I
S2 --> I
SN --> I
I -->|bounded canonical page + qualified IDs| B
```
The scaling layer does four things without forking the protocol:
* **Bilateral calls stay canonical.** Buying mode, brief, filters, proposals,
and refine are ordinary AdCP semantics at each seller edge.
* **Identity survives aggregation.** `sf1:` product IDs and `sfp1:` proposal
IDs prevent cross-storefront collisions and round-trip into purchase.
* **Slow sellers do not erase fast sellers.** Revisioned replacement snapshots
expose seller progress while other agents remain pending. Candidates become
selectable only after their complete seller/source identity is persisted.
* **Responses stay agent-sized.** `pagination.max_results` bounds candidates;
proposal graphs remain whole, and the cursor is frozen only when the
execution is complete.
## Which sellers are searched for an advertiser
Discovery for an advertiser searches only the media partners turned on for that
advertiser: partners selected automatically for its markets and channels,
partners included for the whole account, and partners turned **On** for that
advertiser. Media partners that are off for the advertiser are not contacted
and do not appear in `storefront_results[]`. They are outside the search, not
failed sellers.
When the request names no storefronts, `ext.interchange.storefronts_not_searched`
lists the media partners left out for the advertiser: each one's
`storefront_id`, `storefront_name` and `reason`. `NO_AUTOMATIC_MATCH` means it
does not match the advertiser's markets and channels, `ADVERTISER_DISABLED`
means it is turned off for the advertiser, `ACCOUNT_ALWAYS_EXCLUDE` means it is
excluded for the whole account, and `HARD_INELIGIBILITY` means it cannot
transact with you at all. At most 10 are listed; `total` counts every one. When
you can turn any of them on, `suggestion` says how, once. The `get_products`
text summary names them the same way. Each media partner appears once, even
when it is reached through more than one sales agent. The list is reported only
for an advertiser your account itself owns; for any other advertiser, such as
one another organization delegated to yours, it is omitted rather than
guessed.
A media partner that is off also changes the rest of the response in these
cases:
* **You named it.** When the request names specific storefronts, or refines a
product or proposal from one, a named media partner that is off returns a
`storefront_results[]` row with status `completed`, no products, and an
`outcome` of kind `action_required` with `reason_code`
`advertiser_activation`. Its `message` says why it was not searched and its
`suggestion` says where to turn it on. A partner turned off for the
advertiser is turned on in the advertiser's view of Media Partners (open the
media partner, choose **Controls**, set **Availability** to **On**). A
partner excluded for the whole account is changed in the account view, from
**Always exclude** to **Default** or **Always include**.
* **No media partner is on.** When none is turned on for the advertiser,
discovery searches nothing and `guidance` says so, with the same path to
turn one on.
* **Otherwise**, an empty result's `guidance` also reports how many media
partners were left out because they are not turned on for the advertiser.
A media partner that cannot transact at all is reported as `skipped` when you
named it; turning it on does not change that.
A media partner that is still finishing go-live is different. Some managed ad
servers need the media partner to pass a no-spend certification, such as a
test write to the ad server, before it takes its first booking. Until it
passes, the media partner is searched like any other: its products appear in
results, and you can add them to a buy you are planning. Creating or executing
a media buy with it is refused with `ACCESS_DENIED` and a message saying the
media partner has not finished going live. There is nothing to turn on; the
media partner finishes the certification on its side, and booking opens once
it passes.
Saved results follow the same rule. When a media partner is turned off for the
advertiser after a search, reading that saved discovery no longer returns its
products; an empty page's `guidance` says how many saved products were removed
for that reason. Adding one of its products to a buy, directly or by applying a
proposal, is refused with `ACCESS_DENIED` and a message saying where to turn
the media partner back on.
## Read each seller result honestly
`ext.interchange.storefront_results[].outcome` is present when a seller was
queried through its own agent, or when a seller you named was not searched. It
is one buyer-safe terminal outcome: `returned_products` (with its count),
`no_matching_products`, `no_inventory_for_request`, `action_required` (a named
seller that is off for the advertiser, with its fix), `skipped` (with a safe
reason), `failed` (with a safe reason), or `local_refusal` (the request was
refused by Apostra's own validation before it ever reached that seller, with
its `code`, `message`, and an optional `suggestion` for correcting the
request). A seller with no active wholesale inventory is
`no_inventory_for_request`, not a failed seller and not a retry instruction.
An empty seller result becomes `no_matching_products` only after that seller
has completed without an error. A seller response with an error and no products
is `failed` when no cached catalog can serve the request, even when the seller
marks its task completed. `local_refusal` is never counted toward that
seller's queried/completed total, because the seller was never actually
contacted. An incomplete result remains `incomplete` and can carry a safe
seller clarification instead.
When one agent covers several sellers, `outcome` and `filter_result` are
omitted because that agent-level result cannot be attributed honestly to an
individual seller. Those rows carry product-derived counts and
`property_coverage[]` only. Agent-level outcomes for that case are tracked in
AI-10300.
Returned products also produce `property_coverage[]`: each property domain and
the number of returned products carrying it. Treat this as coverage of the
seller response, not an exhaustive marketplace claim. Each seller row includes
at most 12 property domains, with `property_coverage_total` and
`property_coverage_truncated` when the buyer needs to recognise an abbreviated
list. When a seller reports
filter accounting or unfinished work, `filter_result` says whether no products
matched, filters excluded every candidate, or the result is partial. Seller
diagnostics and operational details are not exposed here.
The advertiser's property exclusion list is applied after seller products are
returned. `ext.interchange.excluded_product_count` is the total number removed,
and each storefront row exposes its own
`storefront_results[].excluded_product_count`. Each seller row also has a
bounded `excluded_products[]` list with the removed product ID, name, and the
matching domain or domains. These domains are the intersection with the
buyer's own exclusion list, never the rest of that list. If
`excluded_products_truncated` is true, use the count as the complete total and
describe the listed products as a bounded sample. If exclusion removes every
completed candidate, read the response guidance and report that **N products
from M sellers were removed because they included properties on the
advertiser's exclusion list**. If a legacy candidate has no seller identity,
the total still includes it but no seller row does; report the removed-product
count without inventing a seller count. Do not call either outcome "no matching
inventory".
`get_products` does not stop after the first 10 storefronts or choose a final
cohort by response speed. When no storefront selector is supplied (or
`storefronts: "all_connected"` is explicit), Apostra resolves every
storefront in the buyer's eligible discovery scope and queries each backing
sales agent once. An ordinary provisional revision contains only sellers that
have answered so far and returns `pagination.has_more: false` with no
`pagination.cursor`; poll the same `execution_id` with `since_revision`
until `results_complete: true`. Only then follow `pagination.cursor` until
`has_more: false` to consume the complete candidate set. This enforces the
stable-cursor contract: incomplete ordinary results are replacement snapshots,
not pages to append. `pagination.max_results` limits each completed response
page, not the seller fan-out. The legacy Discover Products `groupLimit` and
`groupOffset` fields do not apply to this surface.
If the buyer's absolute wait budget ends while Apostra is still assembling the
canonical page (for example, managed evaluation is still running), Apostra
returns the latest persisted replacement snapshot when one exists. That page
keeps the products and storefront results from sellers that already answered,
sets `results_complete: false`, and names the remaining sellers as pending.
The requested processing mode is `degraded`: evaluation requests mark
`ext.interchange.evaluation.evaluated: false`, while screening requests mark
`ext.interchange.screening.evaluated: false`; those candidates were passed
through without the requested instructions. The usual `max_results` and its
server-marked deadline-snapshot pagination cursor still apply to that
replacement page. If no seller snapshot
has landed yet, the same incomplete response is an empty page and says so
without claiming that a seller failed to answer.
For a pure pass-through storefront, a country
filter also narrows the storefront's internal source fan-out after the staged
routing flag is enabled.
Apostra sends the request only to an active source whose own standard
`primary_channels` and `primary_countries` match the requested coverage. A
source with missing or malformed primary coverage is unknown and excluded from
filtered fan-out after activation; before activation, legacy fan-out is unchanged.
Declarations from two different sources are never combined to manufacture a
match. An unfiltered request remains storefront-wide.
Apostra rejects ambiguous seller-specific controls such as conditional
catalog versions on the aggregate surface. Query that seller's AdCP endpoint
directly when you need a seller-specific conditional request.
### Use canonical filter names
Apostra rejects known cross-operation fields inside `filters` before contacting
any seller. Use `filters.countries` to narrow products by country coverage. Put
delivery targeting such as `geo_countries` under `targeting_overlay`, not
`filters`. `excluded_content_categories` is not a supported `get_products`
filter and must be omitted.
Canonical filter fields from the AdCP versions supported by Apostra remain
available. Put seller-specific filter criteria under a vendor namespace in
`filters.ext`, for example `filters.ext.example_seller`. These unsupported
fields return a validation error that names the field and how to correct it.
## Canonical options from older seller formats
### Buyers
When a seller still uses named legacy formats, `get_products` can return the
seller's explicit mapping as a URL-free canonical `format_options[]` entry.
Buyer integrations do not need to change: existing canonical options and
legacy `format_ids[]` keep their current shapes.
When a buyer selects one of these canonical options, Apostra retains the
seller's exact publisher-scoped route for later `create_media_buy`,
`update_media_buy`, and assigned `sync_creatives` delivery. The route is never
reconstructed from a similar format kind or ID. If the exact seller mapping is
missing, malformed, or inconsistent with the returned option, delivery fails
closed and the buyer should refresh products before retrying.
### Sellers
Sellers opt in one format at a time through the catalog returned by
`list_creative_formats`. Add a `canonical` declaration to the legacy row. If
the option needs canonical parameters, include `canonical_parameters` on the
same row and use the same kind:
```json theme={null}
{
"format_id": {
"agent_url": "https://seller.example",
"id": "seller_banner",
"width": 300,
"height": 250
},
"name": "Seller banner",
"canonical": { "kind": "image" },
"canonical_parameters": {
"format_kind": "image",
"params": { "width": 300, "height": 250 }
}
}
```
Dimensions and duration in the canonical declaration must agree with the
legacy format reference. Apostra ignores missing, invalid, conflicting,
ambiguous, or unsupported mappings. If a returned product's older format
remains unresolved, the seller result includes details in
`projection.diagnostics`. Apostra never guesses a canonical kind from a
legacy ID or asset list, and one seller's mapping cannot apply to another
seller.
Products may publish canonical `format_options[]` directly or retain exact
legacy references from a shared format catalog. Use the catalog mapping to
replace seller-specific formats that Apostra cannot interpret.
### Product-format resolvability
When canonical-format compliance is enabled for a storefront, Apostra
surfaces a seller-visible blocker only when a format cannot be mapped to an
interpretable declaration. Exact legacy `{agent_url, id}` references remain
supported without a sunset, for both existing and newly connected sources.
Direct URL-free `format_options[]` are accepted but are not required yet. A
source with no persisted product-format evidence receives an evidence-pending
blocker until its initial catalog sync records declarations. Catalog age is a
separate freshness concern: an interpretable declaration does not become
noncompliant when its cache entry ages. An unresolved declaration remains
withheld until corrected. For a sales agent on the Agent-supplied path,
each live `get_products` response is a brief-specific observation,
not a complete catalog snapshot. The newest response for each public or buyer-account context
replaces that context's previous compliance observation, so products omitted
from a later brief do not keep the source withheld. A buyer that tries to
purchase a previously returned product still has that exact product's
declaration checked before the buy is forwarded. This
policy audits active external third-party sales-agent sources; managed and
modular inventory sources use their own readiness contracts.
A canonical-format blocker withholds the affected source from discovery and
media-buy admission until the seller replaces the unresolved custom reference
with an exact shared-catalog reference or an interpretable custom canonical
declaration. Other healthy sources in the storefront continue selling. If no
other source can serve, the storefront remains transaction-ineligible.
## Choose `get_products` or Discover Products
Both surfaces share the same seller fan-out implementation. They are different
buyer projections, not different integrations.
| Use | `get_products` | Discover Products |
| - | - | - |
| Primary contract | Canonical AdCP products and proposals, plus `ext.interchange` orchestration | Apostra discovery-session view |
| Best for | Buyer agents, protocol-compatible workflows, named/all storefront queries, seller proposals, screening/refine, direct purchase continuation | Existing discovery-session integrations that need grouped pagination, summary/budget context, browse, or SSE |
| Intelligence | Optional absolute proposal screen: accept, reject, or seller refine | The same optional screen; no separate ranking brain |
| Result shape | Flat, bounded `products[]` and optional `proposals[]`, with per-storefront progress metadata | `productGroups[]`, discovery summaries, budget context, and plans |
| Durable continuation | `execution_id` → `create_media_buys` → DRAFT campaign cart | `discoveryId` → legacy selection endpoints or the same campaign reconciliation path |
Start with `get_products` for new agent integrations. Use
[Discover Products](/v2/buyer/discovery/tasks/discover-products) when you need
its legacy grouped/session presentation or need to continue an existing
`discoveryId` workflow. Calling both for the same request is normally
unnecessary; Discover Products projects the same raw seller snapshot rather
than running another ranking implementation.
## Surfaces
* REST: `POST /api/v2/buyer/products/query`
* Buyer MCP: `get_products`
* Buyer `api_call`: operation `get_products`
The purchasable continuation is:
* REST: `POST /api/v2/buyer/media-buys/batch`
* Buyer MCP: `create_media_buys`
* Buyer `api_call`: operation `create_media_buys`
## Continue the selected product query
When the buyer selects returned proposals or products, reuse the exact
`ext.interchange.execution_id` and storefront-qualified IDs from that
`get_products` result. Do not call `get_products` or Discover Products again
merely to attach, re-fetch, or reconstruct the selection. A new discovery is a
different snapshot: its candidates may differ or be empty, and it does not
identify the query the buyer selected from. The documented legacy recovery for
an execution that predates its durable accepted-proposal set still requires a
fresh `get_products` call; treat that response as a new selection and ask the
buyer to select from it again.
If an agent host offers the typed `create_media_buys` tool, call it directly.
On a reduced or shared tool catalog where that typed tool is absent but
`buyer_api_call` is available, use `buyer_api_call` with
`operation: "create_media_buys"`. Both tool routes invoke the same continuation
contract. REST clients call the batch endpoint above. If the conversation no
longer contains the execution ID or a qualified selected ID, report the missing
handle and offer a new discovery as recovery; do not describe that new query as
the original attachment.
## Retrieval without evaluation
Omit `ext.interchange.evaluation` to receive valid seller responses without a
managed evaluation:
```json theme={null}
{
"buying_mode": "wholesale",
"filters": {},
"pagination": { "max_results": 50 },
"ext": {
"interchange": {
"advertiser_id": 123,
"storefronts": "all_connected",
"wait_mode": "quick"
}
}
}
```
Protocol validation always runs. Malformed seller responses, broken proposal
allocations, and invalid identities are rejected before evaluation and do not
consume evaluation work.
### Include products from verified AdCP 2.x sellers
Some sellers still run a verified AdCP 2.x response contract, which predates
the current required `reporting_capabilities` block. Their otherwise valid
products are excluded from the default response because Apostra does not
guess reporting cadence, delay, metrics, or webhook support.
REST and MCP callers can request the separate compatibility response by adding
the following field to `get_products`:
```json theme={null}
{
"buying_mode": "wholesale",
"filters": {},
"ext": {
"interchange": {
"advertiser_id": 123,
"response_contract": "interchange_product_compatibility_v2"
}
}
}
```
The response repeats that contract at
`ext.interchange.response_contract`. A projected 2.x product omits
`reporting_capabilities` and explains the omission explicitly:
```json theme={null}
{
"ext": {
"interchange": {
"compatibility": {
"status": "projected",
"source_contract": "adcp_2_x",
"source_adcp_version": "2.5",
"unavailable_fields": ["reporting_capabilities"]
}
}
}
}
```
Apostra accepts this projection only when protocol negotiation provides a
non-synthetic, seller-declared 2.x version. Unknown, conflicting, synthetic,
or current-version responses still fail closed. The exact seller product
payload—not the buyer-facing projection—is retained for any later purchase.
Omit `response_contract` to keep the original response schema, where every
product requires `reporting_capabilities`. Existing REST and MCP integrations
therefore need no changes. Sellers can remove the compatibility marker from
future discovery by upgrading their declared contract and returning the
current reporting block; no buyer-side account or purchase setting changes.
### Request a smaller product projection
Use the canonical `fields` array when the next step needs only part of each
product. Apostra validates the complete seller response before applying the
projection, then returns only the requested product fields. The qualified
`product_id` and `ext.interchange` provenance remain present so the result can
still be refined or passed to `create_media_buys`.
```json theme={null}
{
"buying_mode": "wholesale",
"filters": {},
"fields": ["product_id", "name", "pricing_options", "delivery_type"],
"pagination": { "max_results": 10 },
"ext": {
"interchange": {
"advertiser_id": 123,
"storefronts": "all_connected"
}
}
}
```
For this request, unrequested product blocks such as `format_options` and
`reporting_capabilities` are omitted. Omit `fields` when you need the complete
canonical product objects.
## Evaluate and negotiate proposals
Add evaluation instructions when you want Apostra to remove unsuitable
proposals, enrich accepted results, negotiate promising proposals, and
optionally rank the accepted cohort before it reaches your agent:
```json theme={null}
{
"buying_mode": "brief",
"brief": "Premium UK sports video with measurable reach",
"filters": { "countries": ["GB"] },
"pagination": { "max_results": 50 },
"ext": {
"interchange": {
"advertiser_id": 123,
"storefronts": "all_connected",
"evaluation": {
"instructions": "Accept proposals that fit the brief and include measurable reach. If CPM is above the stated budget, ask for a lower-cost option. Reject proposals without usable measurement.",
"max_refinement_rounds": 1,
"max_evaluation_passes": 20,
"enrichment": true,
"ranking": {
"instructions": "Prefer stronger measurement and efficient expected reach."
}
}
}
}
}
```
The first evaluation pass is absolute:
* **Accept** returns the seller's unchanged proposal and referenced products,
with optional buyer-owned enrichment.
* **Reject** suppresses the proposal from the normal result.
* **Refine** sends a canonical AdCP refine instruction back to that proposal's
seller, then evaluates the seller's revised response again.
Apostra never rewrites seller pricing, allocations, availability, or terms.
Any changed proposal must come back from the seller.
Autonomous refinement is bounded by `max_refinement_rounds`, repeated-request
detection, the request deadline, and `max_evaluation_passes`. A proposal still
requesting refinement at the limit is rejected rather than silently accepted.
When `ranking` is present, a separate comparative pass scores accepted
candidates under its optional instructions. The response is ordered by score
and includes a rank and rationale. Provisional rankings replace earlier
snapshots as sellers settle; the final completed revision is frozen.
The platform selects the managed models. This version does not expose provider
model names, customer executable code, or a model picker.
`ext.interchange.screening` remains a deprecated compatibility alias for
accept, reject, and refine. It does not enable enrichment or comparative
ranking. Do not send `screening` and `evaluation` together.
## Discover Products compatibility
`discover_products` is a convenience view over the same seller fan-out and
evaluation engine. Passing its optional `evaluation` field enables the same
disposition, enrichment, refinement, and ranking behavior, projected into the
Discover response shape. Without `evaluation`, Discover preserves its existing
enrichment, relevance, ordering, and refinement behavior. Existing Discover
clients do not need to migrate to keep their current behavior.
## Proposals and products
Canonical AdCP proposals reference sibling products through allocation
`product_id` values; they do not duplicate complete product objects. Evaluation
resolves that graph automatically. A top-level product that is not allocated by
any proposal remains a proposal-less candidate, principally for wholesale
catalogs.
`pagination.max_results` bounds the combined number of proposals and
proposal-less products. Products referenced by a paged proposal accompany that
proposal and do not consume another candidate slot, so the proposal graph is
never split across pages. Apostra accepts at most 50 allocated products in
one proposal. A response contains at most 100 total proposal and product objects;
when complete proposal graphs reach that limit, the page returns fewer proposal
candidates and the cursor continues from the next whole proposal.
Both IDs are opaque and storefront-qualified:
```json theme={null}
{
"products": [
{
"product_id": "sf1:42:...",
"ext": {
"interchange": {
"storefront_id": "42",
"source_product_id": "seller-product-7"
}
}
}
],
"proposals": [
{
"proposal_id": "sfp1:42:...",
"allocations": [{ "product_id": "sf1:42:..." }],
"ext": {
"interchange": {
"storefront_id": "42",
"source_proposal_id": "seller-proposal-9",
"evaluation": {
"disposition": "accept",
"evaluated": true,
"confidence": 0.86,
"reasons": ["Fits the brief and measurement requirements"],
"enrichment": {
"summary": "Premium UK sports video with measurable reach.",
"strengths": ["Strong measurement"],
"concerns": [],
"labels": ["video", "sports"]
},
"ranking": {
"rank": 1,
"score": 0.91,
"rationale": "Best combination of measurement and reach.",
"provisional": false
}
}
}
}
}
]
}
```
Pass qualified IDs back unchanged in refine and media-buy creation calls.
When more than one seller or storefront is in scope, Apostra rejects an
item-scoped refine instruction that uses an unqualified product or proposal ID.
Request-scoped refine instructions still apply across the request.
Two storefronts can expose the same upstream product and therefore carry the
same `ext.interchange.source_product_id`, including the same `wh:` value.
Treat the top-level qualified `product_id` as the identity, and use the
`storefront_id` and `storefront_name` from that same product when displaying
provenance. Never merge or relabel results by `source_product_id` alone.
### The targeting a product lets you set
A product can carry AdCP 3.2 `overlay_support`: the targeting dimensions you
may set on a package for that product, such as countries, regions, metro
systems like Nielsen DMA, or a frequency cap, with any per-package limits the
seller declares.
* **Wholesale results** (`buying_mode: "wholesale"`) return the seller's
product as the seller declared it, including its `overlay_support`.
* **Brief results** return products the storefront composes from its
inventory. A composed product carries the `overlay_support` of the inventory
product it is built from, so both modes describe the same inventory the same
way. A product composed before its inventory declared targeting carries it
once the storefront recomposes it.
* A product with no declaration, or one that isn't a valid AdCP 3.2
`overlay_support` (empty, or containing only `ext`), has no `overlay_support`
field.
## Stage or buy the result
The `execution_id` in the response is also the durable search/refinement
record used by `create_media_buys`. Choose whole proposals and/or
proposal-less products from that execution. The operation stages them on a
DRAFT campaign—the campaign is the shopping cart and the parent of every
media buy.
Only proposals in the execution's durable accepted set can be selected. Run
`get_products` again before purchasing from an older execution that predates
that set.
Use an existing campaign cart:
```json theme={null}
{
"productQueryId": "query-id",
"campaign": { "campaignId": "campaign_123" },
"mode": "stage",
"selections": [
{
"kind": "proposal",
"proposalId": "sfp1:42:...",
"totalBudget": 40000
},
{
"kind": "product",
"productId": "sf1:57:...",
"pricingOptionId": "cpm-standard",
"budget": 10000,
"bidPrice": 8.5
}
]
}
```
Or provide `campaign.create` with the same required fields as a discovery-mode
campaign (`advertiserId`, `name`, `flightDates`, and `budget`). Apostra
creates the DRAFT campaign and returns its ID. Media buys are never standalone,
even when the caller did not create a campaign first.
* `mode: "stage"` updates the cart and prepares DRAFT media buys without
contacting sellers.
* `mode: "execute"` performs the same preparation, then sends each resulting
buy to its originating storefront through ordinary bilateral AdCP
`create_media_buy` calls.
* `replace: true` replaces the cart selection before applying this batch;
otherwise selections merge idempotently.
Use `mode: "execute"` when the selected result can be submitted immediately.
When you need to inspect or customize per-buy creatives, dates, pacing, or
optimization goals, use `mode: "stage"`, make those changes on the returned
DRAFT media buys, then call `execute_campaign` or repeat `create_media_buys`
with the same campaign and selections using `mode: "execute"`. Execution
submits the prepared DRAFT directly; it does not rebuild it from the product
query.
One marketplace call may reach several sellers, but the seller calls are not
one atomic transaction. The response reports `mediaBuysExecuted`, `success`,
and per-buy `errors`. Retry the same request after a partial failure; completed
buys are not dispatched again. A completed product query accepts only an
unchanged retry of its recorded batch. To change the selection after execution,
start a new `get_products` query.
See [Create media buys from a product query](/v2/buyer/discovery/tasks/create-media-buys)
for the complete request and response contract.
## Progressive responses
Inside Murph, the runtime retains the original execution and request arguments,
reads cumulative results with `since_revision: 0`, and follows producer-issued
frozen cursors before attaching a selection. Its bounded wait can end with an
incomplete result; that does not mean the query found no products. See
[Murph continuation and attachment](/v2/buyer/discovery/tasks/browse-products#response)
for the wait limit, confirmed retries, and recovery behavior.
Seller responses arrive independently. Each response is a replacement snapshot:
* `revision` increases as sellers settle or a refinement produces a new version;
* `provisional: true` means replace your previous displayed result;
* `pending_agents` identifies sellers still working;
* fresh provisional revisions may contain seller progress but no candidates
while typed product identity is still being persisted;
* proposals being refined are withheld;
* ordinary incomplete responses set `pagination.has_more: false` and issue no
cursor; poll the `execution_id` with `since_revision` instead. No stable
cursor is issued until `results_complete: true`;
* `storefront_results[].message` preserves a seller's explanation for a
successful empty response, including when readiness prevents that storefront
from transacting; do not interpret that case as “no matching inventory”;
* `storefront_results[].error: "product_persistence_incomplete"` means the
storefront answered, but one or more candidates could not be persisted with
a complete seller/source identity. Those candidates and any proposals that
allocate to them are withheld. Safely persisted candidates from the same
execution are still returned; start a new query to retry the withheld
inventory. If no safe candidate remains, the request returns a retryable
service-unavailable error instead of an authoritative empty catalog;
* the final cursor is scoped to the execution, revision, evaluation instructions,
and offsets.
Response speed affects which sellers are visible in an early provisional
revision, not which sellers are retained in the completed execution. Without
comparative ranking, completed products use deterministic storefront-qualified
identity ordering; when `evaluation.ranking` is present, the accepted cohort is
ordered by that ranking instead.
Poll with the returned execution identity:
```json theme={null}
{
"buying_mode": "brief",
"ext": {
"interchange": {
"execution_id": "query-id",
"since_revision": 3
}
}
}
```
Do not append provisional pages. Replace the prior page with the new revision.
Evaluation is idempotent by candidate version and instruction fingerprint.
Polling an unchanged revision does not rerun or remeter it. A seller's changed
proposal is evaluated as a new candidate version.
## Evaluation metadata and cost
The response reports evaluation separately from seller progress:
```json theme={null}
{
"ext": {
"interchange": {
"evaluation": {
"enabled": true,
"status": "complete",
"model_class": "fast",
"evaluation_passes": 2,
"ranking_passes": 1,
"candidates_evaluated": 17,
"candidates_ranked": 8,
"accepted": 8,
"rejected": 7,
"refining": 0,
"refinement_limit_reached": 2,
"estimated_ius": 3,
"charged_ius": 0,
"billing_status": "calibrating"
}
}
}
}
```
One **Proposal Evaluation Pass** evaluates a bounded batch of at most ten
candidates. One **Comparative Proposal Ranking Pass** ranks at most 100 accepted
candidates. We measure these value units rather than provider tokens. Both are
currently calibrating, so `charged_ius` remains `0`. Retrieval without
evaluation reports `billing_status: "included"`.
If managed evaluation is unavailable, valid candidates pass through and the
evaluation status becomes `degraded`; an infrastructure failure does not silently
reject seller supply. Every pass-through candidate is explicitly marked
`ext.interchange.evaluation.evaluated: false` (with `confidence: 0`), and the
response `guidance` says so. Check `evaluated` before treating an accept as a
judgment: `evaluated: false` means your evaluation instructions were **not**
applied to that candidate — re-run the request or apply your own screening
before buying on it.
# Moderation
Source: https://docs.apostra.com/v2/buyer/moderation/overview
Pre-check content against the moderation policy before submitting it
Moderation lets a buyer agent run text through the content-moderation engine **without blocking**, so it can validate input before submitting it to a blocking surface like campaign brief screening. The check returns structured findings — category, severity, and a remediation suggestion — so the agent can self-correct and avoid a `422` on the real call.
It mirrors the same engine that powers `POST /campaigns` brief screening, so a pre-check result predicts how the brief will be treated at submission.
All requests use the base URL `https://api.apostra.com/api/v2` and a `Authorization: Bearer $SCOPE3_API_KEY` header.
## Task Reference
`POST /moderation/check` — pre-check text and get findings
Per-operation reference
# Consume reporting
Source: https://docs.apostra.com/v2/buyer/reporting/consuming-reporting
Use current delivery reads for pacing, and understand the finality and notification boundaries.
Use delivery reads today to understand reported pacing. Do not use them as a
claim that a report is final, complete for billing, or delivered to a separate
destination. Reliable Reporting is in controlled staging; its status,
notification, delivery-evidence, and receipt features are not public API, MCP,
or SDK features.
## Pick the read that answers your question
| Your question | Use today | What it does not answer |
| - | - | - |
| How is seller-reported campaign or media-buy pacing? | V3 `get_delivery` or the buyer reporting metrics endpoint for a bounded period. | Whether every expected source has reported, whether the result is final, or whether it is billing-eligible. |
| What is a connected provider reporting right now? | V3 `get_delivery` with `report: "live_campaign_delivery"` for one exact campaign and a bounded range. | Reporting-store finality, revision history, or billing eligibility. |
| What happened in the buyer workflow? | [Audit logs](/v2/guides/audit-logs). | Delivery performance or reporting finality. |
The reporting metrics endpoint and V3 compatibility reads are seller-reported
delivery. `live_campaign_delivery` is read from the connected provider for
that call and does not use the reporting store. Neither path makes a current
timestamp or scheduled source cadence evidence that all delivery has arrived.
Keep the requested date range with the result, and treat missing source or
package data as incomplete rather than zero.
## Finality is not inferred today
`SNAPSHOT` and `OFFICIAL` are the finality classes Reliable Reporting is
preparing. A snapshot can change; an official result is the source's intended
final result for its reporting period. Both can later be superseded by a
restatement.
The current V2 compatibility data behind V3 `get_delivery` does not include
ordered revision evidence. Its finality result is therefore unavailable and its
revision is `null`; Apostra does not infer `SNAPSHOT` or `OFFICIAL` from fresh
data, a source response, or a delivery cadence.
A source may provide `is_final` or `finalized_at` for a package-level delivery
response. That alone is not an overall finality decision for a media buy or
campaign that spans more than one source. The planned Reliable Reporting rule
is that a combined result is final only when every required source leg is
final. That cross-source rule is controlled staging only.
## Status, corrections, and receipts
There is no public `get_reporting_status` read today. There is also no public
reporting revision ledger or receipt that proves a report was delivered,
received, checked, or eligible for billing. A correction in the current
pipeline can replace the current delivery value without exposing a public
restatement history.
Use current delivery reads for operational pacing, then retain your own
reconciliation records until the public Reliable Reporting contract is
available. See [Reading reporting](/v2/buyer/reporting/overview) for the
available reporting endpoints.
## Reporting notifications are not available yet
The planned Reliable Reporting notifications are:
* `reporting.status_changed` for a committed reporting health change.
* `reporting.delivery_ready` for a verified managed delivery.
* `reporting.ledger_changed` for a receipt or reconciliation change.
They are not public events today. Do not configure a reporting-specific
`reporting_webhook` or build a consumer around these names yet. Existing
[buyer webhooks](/v2/buyer/webhooks) remain a separate contract and do not
provide Reliable Reporting status or receipt evidence.
When the staged notifications are public, they will be compact signals rather
than report payloads. A consumer will use `get_reporting_status` to recover
the authoritative state after duplicate, out-of-order, or missed events. There
is no runnable public SDK example for that flow until the API and SDK are
released.
# Reporting
Source: https://docs.apostra.com/v2/buyer/reporting/overview
Read delivery metrics and hourly event counts for advertisers and campaigns
Reporting endpoints return delivery numbers — impressions, spend, clicks, conversions, and derived rates — rolled up across the **advertiser → campaign → media buy → package** tree, plus hourly counts for events tracked through your own pixel or event sources.
`GET /reporting/metrics` is the single, day-grain endpoint for delivery rollups and CSV exports. `GET /advertisers/{advertiserId}/events/summary` is the hour-grain endpoint for raw event counts. For the full model — the hierarchy, available metrics, CSV export, and how delivery data lands in the reporting pipeline — see the [Reporting overview guide](/v2/guides/reporting-overview).
Reporting preserves the [channel group](/v2/buyer/campaigns/channel-groups)
that compiled into each media buy. Summary responses return `channelGroup` on
each media buy, time-series responses return `channelGroupId` and
`channelGroupName` on each row, and CSV exports append both fields. Legacy or
ungrouped buys return `null` in JSON and blank CSV fields; Apostra does not
infer a group from delivery. REST callers can filter with `channelGroupId`.
To group an agent-side campaign-delivery report, use V3 `get_delivery` with the
`channel_group` dimension and optional `filters.channelGroupId`.
To group campaign delivery by the media channel of what actually ran (CTV,
online video, display, and so on), use V3 `get_delivery` with the `channel`
dimension. Each row returns the AdCP media channels declared by the product
behind its package, falling back to the channels its format options declare,
or `null` when neither declares any. See
[Group delivery by media channel](/v2/guides/reporting-overview#group-delivery-by-media-channel).
For a buyer agent using the V3 `get_delivery` query, campaign delivery can also
group rows by an account-owned label axis with `dimensions: ["labels."]`. The row uses the current labels on the campaign and its
advertiser; when neither has a value for that dimension it is grouped as
`"Unlabeled"`. Labels are resolved when the report is read, so changing a
label reorganizes the full delivery history rather than only future delivery.
An unknown, retired, or inapplicable key is rejected with the account's
available dimensions.
All requests use the base URL `https://api.apostra.com/api/v2/buyer` and a `Authorization: Bearer $SCOPE3_API_KEY` header.
## Task Reference
`GET /reporting/metrics` — hierarchical or time-series delivery metrics
`GET /advertisers/:id/events/summary` — hourly event counts by type
The hierarchy, metrics, CSV export, and delivery flow
Per-operation reference
# Listings
Source: https://docs.apostra.com/v2/buyer/storefronts/discovery-card
Understand the buyer-visible storefront identity, coverage, policy disclosure, and Marketplace filters
A **listing** (previously called the media kit or Discovery Card) is the
buyer-visible projection of a storefront. The same
listing powers Marketplace results and the `discoveryCard` object returned by the
buyer storefront APIs. It is not a separate inventory or policy object.
A listing describes the seller/Storefront identity; seeing the listing does
not mean the buyer is connected. In the buyer V3 surface, one `seller` can have
zero, one, or several authorization `connection` objects, and each connection
can expose multiple provider accounts. See
[Media Partners and connections](/v2/buyer/storefronts/overview) for that relationship.
Seller admins publish this same listing from **Settings → Discovery &
distribution**.
The publication identity, logo, colors, and generated icons come from the
values present in the storefront's canonical brand profile when it is
published. Icon generation is not a background step to wait for.
Publishing and downloading the submission package do not require a logo. If the
brand profile has no usable logo, the publisher uploads production-ready logo
assets directly in OpenAI before submitting. See
[Publisher domains and Discovery publication origins](/v2/storefront/publisher-domains#discovery-publication-origins)
for the operator workflow and public-origin behavior.
The **Discovery & distribution** page links to the complete
[OpenAI submission walkthrough](/v2/reference/white-label-chatgpt-app#4-prepare-the-public-submission),
including verification, package contents, manual logo handling, and the steps
that remain in OpenAI.
Visibility and account access are separate. **Public** makes the published
Discovery page readable on the web; it does not grant anonymous or self-serve
access to the account. Browser-based MCP apps still need OAuth, PKCE, an exact
registered redirect URI, and an account-approved browser origin.
Public distribution also requires an active publication hostname. Apostra's
first-party publication uses the platform-managed `api.apostra.com` origin
and needs no CNAME. A customer-owned origin uses the TXT ownership and CNAME
routing records shown in **Discovery & distribution** before it can become the
preferred public origin. The machine-readable listing JSON is available
under **Developer details** for renderers and integrations; it is not a manual
submission artifact.
## Public root and agent discovery
A verified customer CNAME is one shared publication implementation, not a copy
of the API or a new set of routes maintained for that customer. Its root stays
human-first while publishing machine links in both the HTML and HTTP `Link`
headers. Listing assets stay on the verified publication origin; reusable skills
stay on the platform API origin:
| Path | Purpose |
| - | - |
| `/` | Human listing with a small **For agents** footer. |
| `/card.json` | The exact machine-readable listing revision. |
| `https://api.apostra.com/skills/index.json` | Public, versioned workflow catalog. |
| `https://api.apostra.com/skills/buy-from-seller/SKILL.md` | Workflow for finding and using this publication's verified seller-specific AdCP action. |
The platform-managed Apostra listing uses `/discovery` and
`/discovery/card.json`; agents should follow the advertised links instead of
assuming either layout.
The public listing and skill do not grant access. A buyer authenticates as its own
entitled Apostra identity. To transact directly with the published Seller,
the agent follows the verified `actions[kind=adcp].url` from the listing. On a
Seller CNAME that URL is `/adcp/mcp`, because the verified hostname already
identifies the Seller. A platform-managed listing uses the shared-host fallback
`/seller/{platformId}/mcp`. The agent must not derive a Seller ID or restore the
retired `/storefront/{platformId}/mcp` path.
The action is included when the Seller has the Listing + Distribution package;
other published Listings do not advertise a direct transaction endpoint.
The public projection follows the Seller's current profile immediately, so
changing profiles does not require republishing the Listing.
The CNAME changes public discovery and routing only. Apostra remains the
agent of record for the resulting inbound AdCP campaign: it anchors buyer
identity, advertiser, contract terms, governance, journal, reporting identity,
accepted commitments, and delivery evidence. The Seller Account manages the
one-to-one buy; the public hostname does not bypass Apostra or create a new
commercial counterparty.
`/mcp/v3` is a different contract: it is the account-resolved Apostra
product API. The authenticated account, enrollment, and capabilities determine
which nouns and tools appear in `tools/list`. Serving V3 through an approved
hostname does not make it a direct Seller endpoint, and the hostname does not
widen the credential's authority.
Use the listing to answer three questions before sending a brief:
* Who operates this storefront?
* Does it accept briefs for this channel and country?
* Has the seller chosen to disclose any Advertising Policies?
## Coverage declarations
`coverageDeclarations` contains the source-specific coverage claims. Each entry keeps
the channels and countries declared by one source together. A managed
storefront normally has one declaration; a storefront whose sources are all
external sales agents can have one declaration per active upstream inventory
source. Never combine channels
from one declaration with countries from another.
The top-level `channels` and `countries` arrays are aggregate search and display
facets. They are the union of known values, not a promise that every listed
channel is available in every listed country. Country values are ISO 3166-1
alpha-2 codes such as `FR`, `US`, and `JP`. The legacy channel alias `audio` is
normalized to the AdCP channel `streaming_audio`.
Use `GET /api/v2/buyer/storefronts?region=FR&channel=ctv` to find storefronts
that accept CTV briefs in France. Legacy market-group filters such as `EMEA`
remain available for older region declarations, but new listings use
country codes. A combined channel-and-country filter matches only when one
declaration satisfies both known dimensions. During the compatibility rollout,
missing country coverage remains eligible for country-filtered discovery so
existing storefronts do not disappear. Known country restrictions still apply,
and an explicitly empty authoritative declaration from a pure pass-through storefront remains ineligible.
The `coverage` object makes completeness explicit:
| Value | Meaning |
| - | - |
| `declared` | The storefront has an authoritative declaration for this dimension. |
| `partial` | Some sources declare the dimension and others do not. |
| `unknown` | Apostra has no authoritative declaration. Country filters temporarily keep these storefronts eligible for rollout compatibility; the listing does not claim worldwide acceptance. |
`acceptsAllCountries: true` remains the only explicit worldwide declaration.
For managed storefronts that still have legacy unknown country coverage, Seller
Setup starts the editor with every country selected. Saving records that
worldwide behavior explicitly; leaving the old record untouched preserves the
temporary compatibility behavior above.
## Where listing fields come from
The listing carries `provenance` so buyers and agents can distinguish operator,
registry, and upstream facts.
* Managed storefronts author their accepted channels and countries in Seller
Setup. Their identity is resolved from the verified brand domain and its
brand profile, with an operator-authored Marketplace description.
* Official provider adapters always use the provider name registered by
Apostra and the channels implemented by that adapter. Stale
operator-authored channels cannot change the provider's listing coverage.
Apostra uses the provider logo when it can resolve that logo from the
provider's registered domain. Until then, the listing shows the provider's
initials; it never pairs the provider name with the operator's logo or
colors. The brand domain remains a separate ownership fact.
* A pure pass-through storefront projects coverage
from that source's standard AdCP `media_buy.portfolio.primary_channels` and
`primary_countries`. Those values are read-only in Apostra because the
upstream sales agent is authoritative. The listing turns each source's two
arrays into one correlated `coverageDeclarations` entry; accepted countries
equal primary countries, and separate sources are never combined. A mixed or
BOTH-capable storefront's Storefront-built sources instead declare coverage
locally.
Brand identity remains owned where its `brand.json` is hosted. Seller Setup
opens the specific AAO brand Page for an AAO-hosted identity. For a self-hosted
identity, it instead tells the seller to update `/.well-known/brand.json` on the
verified brand domain.
All Marketplace listings and buyer storefront responses use this same projection;
clients do not need to resolve a second profile to reproduce the listing.
## Advertising Policies
`advertisingPolicies` is an optional buyer-facing disclosure. A managed seller
chooses whether to publish its Brief Acceptance rules, Creative Policy, or
both. Approval routing, approver identities, revision notes, and per-buyer
overrides are never included. A pure pass-through storefront can expose the upstream AdCP
`media_buy.portfolio.advertising_policies` value read-only; a mixed or
BOTH-capable storefront's local disclosure remains editable.
The complete disclosure is bounded to 10,000 characters. Storefront list
responses use `hasAdvertisingPolicies` plus a bounded
`advertisingPoliciesExcerpt`; retrieve storefront detail when you need the
complete disclosed text.
Advertising Policies help evaluate fit, but absence is not permission. The
seller or upstream agent still decides whether to accept a brief.
## Response fields
| Field | Purpose |
| - | - |
| `displayName`, `logoUrl`, `logoBackground`, `brandColors` | Resolved storefront identity. |
| `description`, `website` | Buyer-facing summary and verified destination. |
| `channels`, `countries` | Aggregate search and display facets; never a cross-product coverage promise. |
| `acceptsAllCountries` | True only for an explicit worldwide declaration. |
| `coverageDeclarations` | Source-correlated channel and country claims. |
| `coverage` | Whether each coverage dimension is declared, partial, or unknown. |
| `advertisingPolicies` | Optional complete policy disclosure on detail responses. |
| `provenance` | Source of identity, coverage, and policy values. |
## V3 compatibility
For new V3 integrations, read the listing with
`get({ kind: "seller", include: ["listing"] })` (`discoveryCard` is a
deprecated alias) or open the `listing` Page (`discovery_card` is an
accepted, deprecated spelling of the same Page; `open_page({ page:
"media_kit" })` is retired as an ambiguous key and returns an error naming
`listing` and `business_profile` instead of opening either). The older
`get(kind: "media_kit")` and `save_media_kit` contracts remain available as
deprecated V3 compatibility surfaces pointing at the legacy Business Profile;
they do not define the canonical listing model.
Filter Marketplace storefronts by country and channel.
See how storefronts, inventory sources, and Agent-supplied routing relate.
# Media Partners
Source: https://docs.apostra.com/v2/buyer/storefronts/overview
Browse media partners, connect provider accounts, and manage advertiser activation
A **storefront** is a publisher's buyer-facing home on Apostra — a single ID that aggregates one or more **inventory sources**. A source can be an external AdCP sales agent or managed ad-server-backed inventory. As a buyer you browse the storefronts you can transact with and connect the credentials each source or adapter needs so discovery and media buys flow. For the full conceptual model — the Merchandising Agent, Storefront-built vs. Agent-supplied storefronts, OAuth flows, and the seller side — see the [Storefront object guide](/v2/object-guides/storefront).
## Manage media partner connections
The Media Partners view combines discovery, selection, connection setup, and
advertiser activation. It replaces the standalone Marketplace and Connections
destinations. There is one seller catalog and one set of connection records.
Media partner cards display the seller's available brand logo and background treatment
from its marketplace identity.
The two V3 nouns answer different questions. A **seller** is one exact
Storefront commercial counterparty and exists whether or not you have
authorized it. A **connection** is one authorization grant from your buyer
account to that seller. A seller can have zero, one, or several connections;
each connection can discover several provider accounts. For example, one Meta
business authorization is one connection even when it reveals many child ad
accounts. Authorizing a separate Meta business creates another connection.
Media Partners replaced the standalone Marketplace and Connections destinations. There
is one seller catalog and one set of connection records — no separate views.
The same underlying v2 storefront, provider-connection, discovered-account,
advertiser-mapping, and selection records drive the surface.
The Media Partners view keeps five settings and statuses at their correct scopes:
* **Selection** applies to your buyer account and one Storefront. **Global
Market Makers** are selected for every buyer account. **Regional Market
Makers** are selected when an advertiser's country and channel match their
qualified inventory — the Storefront's own declared markets when it has
declared any, or its approved market/channel scope when it has declared
none. **Marketplace sellers** are sales agents available to include
explicitly. Creative, signal, measurement, optimization, and composite
agents do not appear in this seller catalog. **Ready for buying** shows
transaction-ready partners; **All** also shows sales storefronts that cannot
transact yet, with their readiness status. **Marketplace ready** is a
separate readiness fact: it means the
exact Storefront is listed and can transact, not that the buyer selected or
activated it.
Apostra separately reviews whether a Storefront is Marketplace eligible and,
if so, whether it is Market Maker eligible within a maximum market/channel
scope. The seller controls whether its eligible Storefront is published or
opted out through `save_seller` or the Storefront update API. An active Market
Maker entitlement can activate only an eligible
Storefront and only within that approved scope; paying for an entitlement
cannot bypass eligibility. Critical Supply and commercial access are two
entitlement paths, not listing classes. Seller-declared coverage is
supporting evidence rather than authority, and every path remains subject to
publication, marketplace quality, and technical readiness.
For selection, a confirmed seller listing country or explicit all-countries
declaration bounds ordinary Marketplace inclusion. If the seller has not
declared country or channel coverage, the Storefront can still be included,
but its coverage is unconfirmed and no inventory in that country or channel
is promised. A declared list that excludes the requested country or channel
blocks ordinary Marketplace inclusion. A scoped Critical Supply approval
selects the Storefront by default only within the approved markets and
channels; it does not block ordinary inclusion elsewhere. An official
adapter's registered channels remain a technical limit even when the seller
has left its channel list empty. Buyer Discovery applies its own listing and
coverage filters. `get_products` also applies
source coverage before querying a seller, then returns the seller's matching
products.
Related businesses under one organization can appear as separate Storefront
cards. One organization entitlement may cover those Storefronts, but each is
reviewed separately and receives only the markets and channels shared by the
entitlement and its own approved scope. A Critical Supply entitlement belongs
only to the exact reviewed Storefront and does not flow to sibling cards.
An account administrator can always include or always exclude one exact
Storefront. These classifications describe why a seller is offered; they do
not describe how it authenticates.
* **Billing policy** also applies to your buyer account and a Storefront. When a
Storefront supports both consolidated and direct media billing, you can choose
while the policy is unlocked. Direct-only Storefronts require one account-wide
acceptance that applies to current and future advertisers. Accepting direct
billing does not lock the policy. It locks when Apostra dispatches the
first activation for an eligible advertiser, before that activation becomes
Active. A locked policy is read-only; changing it will require a future
billing migration process, which is not part of this rollout.
Operator-auth platforms are always direct: the platform bills your connected
account under its terms, so Media Partners shows the policy read-only instead of
offering a consolidated/direct selector. Connecting your platform account is
your acceptance of that direct billing, so these platforms never ask you to
**Approve direct billing**. Their only activation steps are **Connect
account**, **Reconnect**, and **Map account**.
For a Storefront you have not selected, Media Partners shows the billing that
would apply when you select it: the parties the seller declares, resolved with
no choice of yours yet. A seller that declares no billing uses consolidated
billing. The consolidated/direct selector and **Approve direct billing**
appear only after you select the Storefront, because the choice belongs to
your selection. A Storefront whose sales paths declare no billing Apostra can
use shows **No supported billing**; it cannot be bought until the seller
declares billing Apostra can use.
* **Data sharing permission** applies to your buyer account and one Storefront.
It is a consent gate: enabling it allows that seller to receive the selected
classes of customer data for current and future advertisers, but does not
itself select or send any audience, catalog, or conversion data. Each
advertiser separately registers the specific audiences, catalogs, and
conversion sources used by its downstream workflows. Account-wide permission
and advertiser-level data registration are separate layers. **Turning a
permission off is destructive**: it disables every advertiser's existing
registrations of that data class under the seller, and turning the
permission back on does not restore them — each advertiser must re-register.
Both directions ask for confirmation on the Media Partners page.
* **Activation** is tracked for the buyer account, Storefront, and advertiser.
The advertiser view lets an administrator choose **Inherit**, **On**, or
**Off** for that advertiser only. Inherit follows account selection and
automatic market/channel matching. Account-wide **Always include** and
**Always exclude** remain authoritative; an advertiser preference cannot
override them. Product discovery for an advertiser searches only the media
partners that are on for it; see
[which sellers are searched](/v2/buyer/discovery/multi-storefront-get-products#which-sellers-are-searched-for-an-advertiser).
Turning one advertiser off preserves a shared Seller
authorization while another advertiser still needs it. The account overview
shows the aggregate state beside each advertiser's state and
required action. When an agent-auth seller is selected, Apostra
asynchronously activates every current eligible advertiser and picks up new
eligible advertisers while that selection remains active. Always exclude
prevents new advertiser accounts from being created; it does not erase
historical seller accounts. Operator authorization, account mapping, and direct-billing
acceptance appear as **Action required** overlays within any classification;
they are not separate seller categories. A selected Storefront with no
advertisers is **Not applicable**, not inactive.
* **Enhanced Reporting** applies to one exact connected account, never to every
account on a seller connection. Turning it on records that account's reporting
preference; an existing reporting subscription starts its reporting and history
pipeline. A billing period earns the published account-reporting rate only
after that exact account completes a successful subscription sync that started
after the control was enabled. An account with no subscription or successful
sync draws no IUs. Turning the control off stops future work while retaining
existing reports and billing evidence. It requires an active IU rate card and
does not affect ordinary connection, account mapping, Buy, Events, Audiences,
or directed campaigns.
Activation can be **Provisioning**, **Active**, **Action required**, **Waiting
on media partner**, **Retrying**, **Failed**, **Retiring**, **Inactive**, or **Not
applicable**. Operator-auth sellers can require **Connect account**, **Reconnect**,
or **Map account** for each advertiser. Direct-only sellers can require
**Approve direct billing**. Agent-auth sellers provision and sync
directly; they do not ask you to select a seller account. A rejection from an
agent-auth seller is shown as a seller outcome, not as an account-mapping
action. If an agent-auth seller reports that an advertiser's account needs
payment, the advertiser shows **Action required**. If the seller rejected,
suspended, or closed the account, it shows **Failed**. For a seller whose
agent lists its accounts, Apostra re-checks that list every few hours, and
every 15 minutes while an account is still under review. If two of the
seller's connected sources list the same account with different statuses, that
account keeps its last status until they agree. An account the seller is
reviewing shows **Waiting on media partner** until the seller approves it, and
an account the seller later suspends, closes, or rejects stops showing
**Active**. The seller still decides whether to accept a buy on that account. A failed row includes an opaque support code that support can correlate
with the durable activation event without exposing credentials. Currency and reporting time zone are advertiser settings; markets and
channels inform automatic seller availability. Advertisers do not choose a
separate billing policy.
An operator authorization such as Meta can expose multiple child ad accounts.
Media Partners shows the complete discovered account inventory, provider status,
Business Manager or other parent when known, mapped advertisers, and last
update. Each advertiser has one active default mapping per seller/source;
changing it preserves the old link for historical buy attribution. A provider
account can be reused across advertisers. Routing one buy across multiple
provider accounts is a campaign-level allocation concern, not an advertiser
activation or seller-selection setting.
Use **Add account** to start the existing provider connection flow again,
**Reconnect** to refresh access, and **Remove connection** to archive one
authorization. Removing a connection does not erase advertiser mappings; a
mapping that no remaining authorization can reach is retained as unreachable
until you reconnect or map another account. Provider account inventory is an
authoritative snapshot from the platform, so Apostra does not offer a
misleading delete button for one discovered Meta ad account.
Sandbox is an advertiser property, not a label Apostra can infer for every
provider account. Create one with **Create sandbox advertiser**; the setup task
opens with sandbox enabled, and sandbox cannot be changed after creation.
Media Partners marks those advertiser rows and selector options as **Sandbox**. Meta
test-account classification is not supported yet, so Meta provider accounts are
not labeled sandbox and a sandbox-only Meta account-list request returns no
production accounts.
Storefront availability and buyer connection state are separate. `readiness.canTransact` is the canonical answer to whether ordinary buyer traffic can purchase now, and `readiness.effectiveStatus` explains whether the storefront is `live`, `blocked`, `paused`, or `archived`. Credentials are still required per source when applicable. List endpoints return `sourceCount` and `connectedSourceCount`; detail returns `connected`, `requiresCredentials`, and `customerAccounts`. Those connection fields describe buyer wiring only and never make a blocked storefront live.
There are two bills: intelligence units (IUs) and media. A seller's media appears on your Apostra media bill only when the storefront uses consolidated billing, advertised as `agent` billing (AdCP `BillingParty` vocabulary: the seller sends its invoice to the agent, Apostra, which bills you on your consolidated media bill and passes your payment to the seller). Apostra is a billing agent, not a guarantor: the seller carries the credit risk on its media. Every storefront list and marketplace card carries `supportedBilling`, and every hosted AdCP capability response carries the protocol-native `account.supported_billing`, so you know the counterparty before you buy. Adapter storefronts advertise `["operator", "advertiser"]`: your connected platform account bills you directly, and Apostra never touches the media money.
## Automatic selection and overrides
Selection answers whether your buyer account should have a connection to one
exact storefront. An eligible storefront can be selected automatically because
it is a Regional Market Maker with qualified inventory in an advertiser's
country and channel, or because a Market Maker-eligible Storefront has matching
Global Market Maker authority. In the reviewed model, Market Maker
eligibility is not itself activation: without a matching entitlement the
Storefront remains Marketplace-visible as **Market Maker eligible**. Critical
Supply or commercial authority activates it only where the entitlement's markets
and channels intersect the approved maximum scope. A seller publication
opt-out removes it from buyer discovery without changing the underlying Apostra
review. Global authority can select the Storefront account-wide even before the
buyer has an advertiser. Selection is account-wide: one selected storefront
connection is reused across advertiser contexts.
An advertiser with no markets set is treated as working in every market, and
one with no channels set as buying every channel. Product requests and media
buys for that advertiser therefore reach every Market Maker that qualifies in
any market or channel. Seller accounts are not created ahead of time for an
advertiser that has left its markets or channels blank. Set them to narrow
automatic selection to where the advertiser actually operates.
Online video (`olv`) is reviewed as its own channel. A Storefront's display or
connected-TV approval does not grant online-video Market Maker coverage; its
approved scope and matching entitlement must both include `olv`.
Market Maker scope uses the AdCP `MediaChannel` vocabulary: `display`, `olv`,
`social`, `search`, `ctv`, `linear_tv`, `radio`, `streaming_audio`, `podcast`,
`dooh`, `ooh`, `print`, `cinema`, `email`, `gaming`, `retail_media`,
`influencer`, `affiliate`, `product_placement`, and
`sponsored_intelligence`. Existing `audio` scope is interpreted as
`streaming_audio`; new scope uses the canonical value.
An account administrator can set an override for the exact storefront:
* **Default** removes the explicit override and returns control to automatic
selection.
* **Always include** selects the storefront account-wide while it remains
eligible, even when your current advertiser footprint has no matching
market-and-channel context.
* **Always exclude** prevents the storefront from being selected.
The override does not apply to sibling storefronts owned by the same seller.
Hard ineligibility, such as an archived or non-transactable storefront, still
wins over an include decision.
Inside one advertiser, Seller activation has a separate scoped preference:
* **Inherit** follows account selection and automatic matching.
* **On** uses the Seller for this advertiser when eligible.
* **Off** does not use the Seller for this advertiser only.
Seller eligibility takes precedence over every account or advertiser setting.
Account **Always include** or **Always exclude** settings override the advertiser
preference until the account returns to **Default**. Changing an advertiser
preference does not create or remove an authorization grant, change another
advertiser, or alter account-wide billing or data-sharing settings. The public
V3 write is `save_connection({ sellerId, advertiserActivation: { advertiserId,
decision } })`, where `decision` is `DEFAULT`, `ENABLED`, or `DISABLED`.
Selection is also separate from activation and billing readiness. A direct-only
or operator-auth storefront can remain selected while its connection reports an
action such as approving direct billing, connecting or reconnecting an account,
or mapping an advertiser. Unsupported or failed activation does not temporarily
turn the selection off. See [Update selection override](/v2/buyer/storefronts/tasks/update-selection-override)
for the API operation.
Storefront names are not always the names buyers use in conversation. The
authenticated storefront list's `name` filter also resolves the seller
account or company, brand, publisher/brand domain, and website. Public Murph
lookup uses only curated storefront name, brand, domain, and website fields for
marketplace-listed storefronts, so questions such as
"Is OptOut's storefront available?" work without supplying an exact domain or
connecting the Slack channel to an account. That public answer contains only
the seller and storefront names, public status, domain, channels, and regions;
buyer credentials and account-specific data still require authentication.
When an Apostra admin declares a Slack channel shared between a buyer and a
seller, Apostra records that relationship as demand and automatically
shows the seller's marketplace-visible storefront relationship in the buyer's
**Supply I would buy** view with the `storefront` channel. These rows are labeled as shared-channel tracking and are not
removable manual requests. Any seller-facing demand summary keeps the buyer's
identity private unless disclosure is separately permitted.
Use `GET /api/v2/buyer/storefronts/:storefrontId/capabilities` when you need a diagnostic view of the active sources behind a storefront. External AdCP sales-agent sources are capability-checkable and include cached-or-refreshed capability details. Managed ad-server-backed sources are returned with `probeable: false` and `probeStatus: "not_applicable"`; do not treat those rows as unreachable agents just because they are not checked through the AdCP capabilities endpoint.
Adapter storefronts can have more than one authorization connection for the
same buyer, and each authorization can reveal more than one provider account.
For example, an agency can authorize separate Snap businesses for different
brands, while either business can expose several child ad accounts. Use the
storefront authorization flow again to add a connection; already-connected
storefronts show this as **Add account** rather than replacing the existing
grant.
Brief-led product discovery separates a platform product from the remaining
campaign decisions. On TikTok, a brief with one clear objective but no country
still returns relevant products and reports `missing_geography` in
`scope3_brief_strategy`; it does not imply that the connected account has no
inventory. A missing or competing objective returns no recommendation until
you choose one. State countries in explicit geographic prose, such as “in the
US” or “markets: GB and AU.” When a Meta or TikTok strategy is ready, its plan
contains the exact returned product and pricing-option IDs plus an immutable
execution snapshot. Activation resources such as a Page, form, Pixel,
identity, destination, or creative remain separate selections and must still
be supplied when the product contract requires them.
Use `buying_mode: wholesale` when you need the complete deterministic catalog
of adapter-executable products. Brief mode composes from that catalog; it does
not replace or narrow the wholesale result. A brief response can keep grounded
products in its relevant shortlist while withholding an actionable plan. In
that case, `scope3_brief_strategy.clarification_codes` identifies unresolved
decisions such as objective or geography; the returned strategy remains
`clarification_required` until you provide the material choice. Product,
capability, signal, and resource IDs in a ready plan must come from the
discovery and account evidence supplied for that request.
A brief can also name an existing audience instead of using the structured
targeting fields — for example, "retarget my VIP Customers list." On Meta and
TikTok, that reference is resolved against the connected account's live
custom-audience inventory: a name that matches exactly one live audience is
applied and appears in the ready plan's `resolved_audiences`, while a name
that is missing, ambiguous across several audiences, or conflicts with
another request for the same audience keeps the strategy at
`clarification_required` and names the issue in `clarification_codes` instead
of silently dropping the reference.
Apostra maintains a versioned synthetic contract corpus across Meta, Google,
TikTok, LinkedIn, Snap, Pinterest, Reddit, and Spotify. Missing adapter
observations fail the portfolio check; the corpus does not imply that every
adapter already returns the shared plan. The check does not contact provider
accounts and does not prove that a connected account is ready to launch.
Continue to use the returned account-resource readiness and execution-package
fields before creating a media buy. This evaluation adds no new response
fields and requires no change to existing API requests.
Meta products that the [Meta compatibility guide](/v2/guides/ad-platform-compatibility/meta)
marks **Testing** are not offered through standard brief or wholesale
discovery, and `create_media_buy` rejects them too. A package that uses a
Testing product's ID — for example, one saved from an earlier discovery —
fails with `UNSUPPORTED_FEATURE` before anything is sent to Meta, so no
campaign is created. Accounts enrolled in Apostra's product testing can
discover and buy Testing products. Products Meta has retired stay
unavailable to every account.
For a Meta, TikTok, or Snap product returned by brief or wholesale discovery,
`execution_readiness.slots` shows those activation resources as they appear on
the connected account. Each slot names whether Apostra selected the only
eligible account resource, needs you to choose among several, needs account
setup, needs campaign input such as a URL or creative, or could not read that
provider inventory. Candidate IDs and names come from the provider account—not
from brief interpretation. A failed inventory read is never presented as “no
resources.” Meta Page candidates are the Pages the selected ad account can
promote, the same Pages a media buy accepts, so discovery never offers a Page
that `create_media_buy` would refuse. Meta Form candidates also identify their
owning Page. TikTok
identity remains a deliberate choice because it must match the selected
creative, while a single eligible Pixel can be selected automatically. Snap
binds the account's Public Profile automatically when present and returns
Snap Pixel candidates for conversion-optimized products; a product without a
buyer-supplied creative or destination is flagged with `input_required` on
those slots.
LinkedIn Lead Generation is not currently available to buyers. The
single-image resource, creative, and readback implementation remains behind a
protected conformance gate until an approved exact-revision no-spend lifecycle
run is retained. It is absent from ordinary product discovery, and ordinary
create requests fail before contacting LinkedIn. Carousel, video, Message,
Conversation, and Document Lead Gen branches are also not executable.
LinkedIn Website Conversions is not currently available to buyers either. The
single-image resource, creative, and readback implementation is present
behind the same protected conformance gate: it binds one or more of the
selected account's enabled conversion rules as `optimization_goals` event
sources and verifies each campaign's conversion association, but it is absent
from ordinary product discovery until an approved exact-revision no-spend
lifecycle run is retained. Carousel and video Website Conversions are also
not executable. Matched Audiences access is a separate, optional signal
source across LinkedIn products; its absence never blocks discovery or
campaign creation.
Wholesale Meta App Promotion products expose the same account resource view
under `product.ext.scope3_execution_readiness`, even without paid brief
composition. Installs, post-install events, and in-app value are separate
products; application, event source, provider-authorized conversion event,
Page identity, creative, and audiences remain separate attachments. This lets
a buyer reproduce the platform UI choice without creating one product row for
every app, event, audience, or targeting combination.
The selected product's
`ext.scope3_brief_strategy.execution_package_template` is the request-ready
portion of that decision. It includes the exact product and pricing option,
geography, resolved audiences, event goal, and authorized Page, Pixel, Form, or
App IDs. Check `remaining_inputs` before execution: URLs, creative,
account setup, and ambiguous choices are intentionally not guessed. Pass a
chosen Meta Instant Form as `leadFormId` in the buyer product selection; it is
forwarded to the adapter with its Page and event goal. Pass a chosen TikTok app
as `appId`; it is durably forwarded to the App Install writer.
For **Meta Sales — Website Catalog Sales**, choose an authorized commerce
catalog and a non-empty product set that belongs to it. The execution package
carries them as `meta_catalog_id` and `meta_product_set_id`, alongside the
selected Pixel/Dataset, Facebook Page, destination, and creative. Catalogs and
product sets are activation resources, never audience signals. A `get_products`
call returns a bounded number of product-set candidates in
`ext.scope3_execution_readiness` across every ad-account-authorized catalog;
once a catalog is chosen, pass its ID as `ext.meta_catalog_id` on a follow-up
`get_products` call to scope discovery to that one catalog. A catalog-scoped
call still returns candidates one page at a time; if the response's
`product_set` slot reports more remain, pass the last candidate's ID as
`ext.meta_after_product_set_id` on the next call to continue from there.
For **Meta Engagement — Event Responses**, the account's upcoming, promotable
Facebook Events are also returned one page at a time in the `destination`
slot; if it reports more remain, pass the last candidate's ID as
`ext.meta_after_event_id` on the next `get_products` call to continue from
there.
For **Meta Leads — Messenger, Instagram Direct, and WhatsApp**, the account's
welcome-message flows are returned the same way in the `welcome_message` slot,
scoped per authorized Page or Instagram profile. This slot always reports
`decision: "explicit_selection_required"` — unlike Page or Pixel selection,
a welcome message is never auto-bound even when exactly one candidate exists,
because it fixes the automated conversation a lead-to-message buy runs.
Candidates only appear on a catalog-style `get_products`/`discover_products`
call with no `brief` — a `buying_mode: "wholesale"` request that still
includes a `brief` does not surface them; a brief-driven call only computes
readiness for the one AI-selected product, and drops it entirely if the
brief needed any clarification. Reads are best-effort per identity: if some
authorized Pages or Instagram profiles couldn't be read within the read
budget, the slot still returns any real candidates the identities that did
succeed found, and `reason` names how many couldn't be read — it is not
silently presented as a complete list. An account with more than 50
authorized identities only sees candidates from the first 50 (a stable,
sorted subset). The slot reports `decision: "inventory_unavailable"` —
instead of a false "no compatible flow" — whenever a read failure (an
identity's own read, or the authorized-identity list itself) leaves zero
candidates found; an account with zero authorized Pages or Instagram
profiles to begin with is a different, successfully-confirmed case and still
correctly reports no compatible flow.
When listing or linking advertiser accounts from an adapter storefront, use the returned `credentialId` to distinguish which connected provider credential owns the account. This is required when more than one connected credential can expose the same upstream `accountId`.
Account discovery is stored as an authoritative provider snapshot. The account
list preserves the provider's normalized and provider-native status, hierarchy,
advertiser label, and ISO currency; these fields are returned consistently by
the buyer REST API and service routes. Only active advertiser accounts can
become the default or selected buying account. Manager containers and accounts that are pending,
payment-blocked, suspended, rejected, or closed remain visible for setup and
diagnosis but are not selectable. If provider pagination or validation fails,
Apostra keeps the previous valid snapshot rather than treating a partial
response—or a storage error midway through publication—as account deletion.
When a credential is replaced while an older refresh is still running, the
older response is discarded even if it completes last; only the snapshot made
with the current credential can replace the stored list. For Meta, a
`sandbox: true` account-list request returns no accounts until Meta test-account
classification is supported, so production accounts are never returned to a
sandbox canary. A database lock or statement timeout during publication also
keeps the prior complete account list. Publication is rejected before changing
the stored list if a provider returns more than 5,000 accounts or the complete
database publication exceeds 30 seconds. During a rolling deployment, an older
application replica can still change which existing account is selected, but
cannot replace authoritative provider account fields without the current
credential proof.
OAuth token refresh publishes a replacement credential atomically. If that
publication fails, the previously connected credential remains active and
usable; the old credential is retired only after the replacement commits.
When a reconnect succeeds, Apostra keeps the selected account and its
stable account identity only if the refreshed grant's complete account list
still contains that account; the account is then rebound to the refreshed
credential. A selected Snap account is not reported as authorized unless that
exact active OAuth credential still owns the account and its stored secret can
be read. Missing, expired, errored, mismatched, or unavailable credentials fail
closed before a provider tool runs and expose the safe diagnostic reason
`delegated_auth_unready`; reconnect Snap to restore the account.
Existing Snap connections require one reconnect after this deployment. This
intentionally establishes fresh account-ownership proof instead of trusting or
backfilling a pre-deployment snapshot. Until that reconnect succeeds, Snap
adapter calls fail closed with `delegated_auth_unready`.
OAuth credentials are never returned in account metadata. Meta financial amounts are converted from the
account currency's minor unit, so zero-decimal currencies such as JPY and
three-decimal currencies such as KWD are not treated as cents. Meta's zero
`spend_cap` value means that no spending cap is configured; it is not reported
as zero available credit.
For Meta products, placement choices use publisher-scoped AdCP placements.
Today the selectable catalog contains Facebook and Instagram Feed, Stories,
and Reels. Name the surfaces in the discovery brief or a refinement request to
narrow the product before buying. If you do not select placements, the adapter
keeps Meta Advantage+ placements as the default. A creative's placement mapping
controls where that creative can render; it does not change the inventory
purchased by the media buy.
Placement publication is fail-closed. Meta publishes six targetable Facebook
and Instagram surfaces. Publisher-contained Spotify products publish included
`MUSIC` inventory. Spotify podcast and TikTok inventory can span publisher
networks, so they are not published under Spotify's or TikTok's domain. Other
adapters do not turn creative-format labels, automatic strategies, or ad
networks into placements until provider selection, containment, and exact
readback are implemented. Flashtalking is creative/ad-serving
infrastructure, not a publisher sales agent. Retail-media inventory must be
scoped to account-discovered retailer domains, never the technology vendor's
domain.
Placement performance is returned only when the product advertises
`supports_placement_breakdown` and the request includes
`reporting_dimensions.placement`. Meta and publisher-contained Spotify Music
products support package-level `by_placement` rows. Spotify verifies that Music
packages have no off-platform impressions; podcast/network packages do not
advertise the capability. Other adapters omit the dimension until their
provider reports can be mapped and reconciled to public placement IDs exactly.
For directed-campaign REST delivery, set `placementBreakdown=true`; optional
`placementLimit` and `placementSortBy` query parameters map to that same AdCP
request.
Adapter credentials are separate from inventory source credentials. Do not use `POST /storefronts/:storefrontId/sources/:sourceId/credentials` to fix an adapter-provider OAuth token, API key, or bearer token; reconnect or rotate the adapter credential through the storefront adapter connection flow for that provider.
For tracked campaigns, a registered external AdCP source can
also be projected into the shared buyer connection plane after its account is
discovered and linked to an advertiser. Use
`POST /storefronts/:storefrontId/sources/:sourceId/adcp-connection`; it does not
accept a new endpoint or secret and does not replace source registration.
## Adapter credential lifecycle
Adapter storefront credential status is maintained after the initial connection. OAuth credentials are refreshed automatically during use and by a nightly health sweep before expiry. If refresh fails because the provider token is expired, revoked, or otherwise rejected, Apostra marks the adapter credential `EXPIRED` and the storefront connection summary reports `error` until the buyer reconnects.
Bearer and API-key adapter credentials cannot be refreshed by Apostra. If they carry an expiry timestamp, the health sweep notifies operators before expiry and marks them `EXPIRED` after expiry. Delegated adapter calls that receive provider auth failures also write back credential health: expired OAuth credentials become `EXPIRED`, while invalid API keys, bearer tokens, or permission failures become `ERROR`.
## What a storefront advertises
A storefront's own AdCP `get_adcp_capabilities` describes whatever serves it:
* **`media_buy.buying_modes`** always includes `brief` and `refine`. It
includes `wholesale` when the storefront can return a wholesale catalog: an
adapter storefront, or a storefront with a source that supplies wholesale
products.
* **`account.require_operator_auth`** is `true` only for an adapter
storefront whose provider requires the buyer's own operator credentials.
A storefront backed by a seller's sales agent does not require it: the
seller connects the agent and maps each buyer account to the agent's own
account (see [Buyer Account Mapping](/v2/storefront/buyer-account-mapping)).
## Key concepts
| Concept | Description |
| - | - |
| Storefront | A publisher's buyer-facing presence aggregating inventory sources behind one `id` |
| Inventory source | A source behind a storefront, either an external AdCP sales agent or managed ad-server-backed inventory |
| `requiresCredentials` | Whether the buyer must register credentials to use a source |
| `readiness` | Canonical transaction availability: `canTransact`, `effectiveStatus`, and machine-readable `blockerIds` |
| `connected` | Whether the source is usable without additional credentials or already has working buyer credentials; not seller readiness |
| Selection | Whether the buyer account should maintain a connection to one exact storefront; independent of activation and billing readiness |
| Selection override | `DEFAULT`, `ALWAYS_INCLUDE`, or `ALWAYS_EXCLUDE` for one buyer account and exact storefront |
| Credential | A buyer's registered account at a source — covers one or more `(storefrontId, sourceId)` pairs |
| `credentialId` | Connected provider credential identifier used to disambiguate duplicate upstream account IDs |
| `displayStatus` | Deprecated stored-control compatibility label; do not use for purchasing availability |
| `sourceCount` / `connectedSourceCount` | Total sources vs. those the buyer has wired up |
## Task reference
`GET /storefronts` — paginated summaries
`GET /storefronts/:storefrontId` — rolled-up connection state
`GET /storefronts/:storefrontId/capabilities` — source diagnostics
`PUT /storefronts/:storefrontId/selection-override` — use Default, Always
include, or Always exclude
`GET /storefronts/credentials` — all your registered credentials
`POST /storefronts/:storefrontId/sources/:sourceId/credentials` — connect an
inventory source, not an adapter provider
Project a mapped source account into tracked campaigns
## Related
Full model: sources, OAuth, seller side
Once connected, run discovery to find products
# Syndication
Source: https://docs.apostra.com/v2/buyer/syndication/overview
Toggle syndication of audiences, event sources, and catalogs to ADCP agents
Syndication pushes a buyer resource — an audience, event source, or catalog — to one or more ADCP agents so they can act on it. You toggle syndication ON or OFF per resource per agent, and each toggle creates a status record that progresses from `PENDING` through `SYNCING` to `COMPLETED` (or `FAILED`).
Toggling returns the status record immediately; because the sync itself is asynchronous, query the status endpoint to confirm a record has reached `COMPLETED`.
All requests use the base URL `https://api.apostra.com/api/v2/buyer` and a `Authorization: Bearer $SCOPE3_API_KEY` header.
## Task Reference
`POST /advertisers/:id/syndicate` — turn syndication on or off
`GET /advertisers/:id/syndication-status` — query status records
Per-operation reference
# Update proposals
Source: https://docs.apostra.com/v2/buyer/update-proposals/overview
Poll and cancel pending media buy update proposals
An update proposal is a pending change to a media buy that awaits resolution by the sales agent. It moves from `PENDING` to either `APPROVED` or `REJECTED`. While a proposal is `PENDING`, you can poll its status or cancel it; once resolved, it is terminal.
Buyer agents using the `api_call` MCP tool can poll with `get_update_proposal` and withdraw a pending proposal with `cancel_update_proposal`, passing the proposal ID returned by `update_campaign` as `pathParams.proposalId`.
Cancellation resolves the pending Apostra proposal as `REJECTED` with `rejectionReason: "cancelled_by_buyer"`, retains a safety record so a later callback cannot apply it locally, and releases the one-pending-update gate. It does not recall a request that Apostra already dispatched to the seller; contact the seller/operator when the upstream task itself must be withdrawn.
Poll the proposal until `status` leaves `PENDING`. When a proposal is `REJECTED`, read `rejectionReason` for the cause. To withdraw a change before the agent acts, cancel it.
All requests use the base URL `https://api.apostra.com/api/v2/buyer` and a `Authorization: Bearer $SCOPE3_API_KEY` header.
## Task Reference
`GET /update-proposals/:id` — poll resolution status
`DELETE /update-proposals/:id` — withdraw a pending proposal
Per-operation reference
# Typed Buyer Tools
Source: https://docs.apostra.com/v2/features/buyer-typed-tools
Give Murph the exact request shape of every buyer operation before it acts
Typed buyer tools are in beta, rolled out per account.
## Overview
When you ask [Murph](/v2/setup/ask-murph) to do something on the buyer side —
discover products, create a campaign, apply a proposal — Murph translates your
request into an API call. By default it does this through a single general-purpose
tool and reasons out the request shape as it goes.
**Typed buyer tools** change that. When your account is enrolled, each covered
buyer operation is presented to Murph as its *own* tool, carrying that operation's
exact request schema. Murph sees the precise fields an operation expects **before**
it makes the call, instead of inferring them.
## What it changes for you
* **Fewer failed operations.** Most tool-call errors come from the assistant
guessing a field name or type. When the schema is attached to the tool, that
class of error largely disappears — the request is shaped correctly the first
time.
* **Same conversation.** Nothing changes in how you talk to Murph. You ask for the
same things in the same way; the reliability improvement happens underneath.
* **Full coverage preserved.** Operations that don't yet have a typed tool still
work through the general-purpose path, so nothing you could do before stops
working.
## Availability
Enrollment is controlled per account while the feature is in beta. To request it
for your account, ask [Murph](/v2/setup/ask-murph) — it can raise the request with
Apostra team.
## Related
The assistant these tools run behind.
# Review Support Channel Participants
Source: https://docs.apostra.com/v2/features/channel-participant-review
Decide what account access, if any, each person in a connected support channel should have
## Overview
People can join a connected Slack support channel without becoming members of
your Apostra account. Channel presence is an observed provider identity,
not authorization to view account data or make changes. Slack is the first
supported participant roster; other messaging providers will use the same
access model when their roster integrations become available.
Account admins can review the participant roster in **Communications**. Until
an admin makes a decision, an unrecognized or foreign-account participant does
not receive account-specific Murph answers. General product and public
storefront questions can still be answered. Keeping someone as a channel guest
or identifying an external company does not grant account access.
## Review a new participant
When a new identity is observed, the roster shows the person's provider name,
email when the provider supplies one, and the connected channel. An account
admin chooses one of four outcomes:
* **Add to account** sends the existing Team invitation with the selected
Basic, Premium, or Admin role. The participant receives account access only
through that invitation and the normal membership checks.
* **Keep as channel guest** allows the person to remain in the provider
channel without granting Apostra account membership.
* **Mark as external company** records the participant as a known
counterparty. A non-empty company name is required for this decision.
* **Disconnect channel** removes the account-owned channel binding and stops
using its participant records. It does not remove anyone who already has a
separate account membership.
A consultant who already has access to your account remains valid even if they
also work with another Apostra customer. Parent and child accounts in the
same organization are treated as one account boundary.
## Shared channels
For a declared shared channel, members of the named counterparty organizations
are expected participants. Anyone mapped only to another organization still
requires review. Marking a known external counterparty keeps account data and
changes unavailable to that participant; it does not turn the provider roster
into an account access list.
## Removal and audit history
Provider removals and channel disconnections remove the participant records
from active use but do not silently change account memberships. Admin decisions are audited with
the actor, provider, installation, channel, participant identity, decision,
and resulting posture.
## Related
Configure a buyer account and its support channel.
Configure seller communications and storefront access.
# Identify a Message Sender
Source: https://docs.apostra.com/v2/features/identify-a-message-sender
Record who an observed email sender is, without granting that person any access
## Overview
When someone emails an Apostra inbox, Apostra keeps the message as a Session —
a durable conversation and its working context — before anyone knows who wrote
it. The sender is retained as an **observation**: the address Apostra saw on
the envelope. An observation carries no account membership, no verified
identity, and no permission to act.
Account admins can say who that observed sender is. A decision names a
customer, an advertiser, or a user, and Apostra keeps it as a dated, attributed
record of how the conclusion was reached.
Identifying a sender resolves **who they are**. It does not grant access, move
the conversation into an account, change who can read it, or act on anything
the sender asked for earlier. Claiming a conversation and granting account
access are separate, separately authorized operations.
## What you can see
Your admins see senders observed on Sessions that belong to **your own
account**. A message that reached a shared Apostra inbox rather than your
account's inbox belongs to that receiving integration and is not visible to any
customer; only Apostra operators can identify those senders.
Each entry shows the transport the message arrived on, the observed address,
the display name when the message carried one, when the sender was first and
last observed, how many messages Apostra has seen from them, and every link
decision that currently stands.
Filter by `unresolved` to see only senders with no standing confirmed link —
the queue of people still to identify.
## Recording a decision
A decision has a state and a target.
**States**
* `candidate` — a proposed identification that nobody has confirmed. Several
candidates can stand at once.
* `confirmed` — the standing answer.
* `rejected` — this target is not who the sender is.
* `revoked` — a previously confirmed link that no longer holds.
**Targets**
* `customer` — the account the sender belongs to.
* `advertiser` — one of that customer's advertisers.
* `principal` — a user who is a member of that customer.
### Rules that apply
* **You must be an account admin.** Other members cannot see the list or record
a decision.
* **Identify the customer first.** On a shared-inbox Session, an advertiser or
user cannot be linked until a customer is confirmed for that sender. On your
own account's Session the customer is already your account, and naming a
different one is refused.
* **One customer at a time.** A sender can carry several candidate customers,
but only one confirmed one. Revoke or reject the standing confirmation before
confirming a different customer.
* **The target must be real and related.** The advertiser must belong to that
customer and not be archived; the user must hold an active membership of it.
* **Decisions are never edited.** Changing your mind records a new decision
that supersedes the previous one. The earlier decision stays readable and is
reported as `superseded`.
* **Evidence is chosen by Apostra, not by you.** Each decision is bound to a
retained message in that conversation, so a later conclusion can never appear
to have been known to an earlier message.
* **Record decisions as yourself.** A decision made while impersonating another
user is refused, because the record names the person who made it for as long
as it is kept.
Send a `clientRequestId` you generate with each decision. Repeating it returns
the original decision unchanged, so a retried request never records a second
conclusion.
## Availability
This is an early capability, enabled per account. Email intake is not yet
generally available, so most accounts will have no observed senders to
identify. Contact your Apostra representative if you want your account enrolled.
## Related
The same access model for people observed in a connected support channel.
Definitions for advertiser, principal, and account membership.
# Shared Murph Rooms
Source: https://docs.apostra.com/v2/features/murph-shared-rooms
Bring teammates into a Murph conversation with a shareable room link
## Overview
A shared room is a Murph conversation that more than one person can open at
the same time. Turn sharing on, send the link to a teammate, and they land in
the exact same conversation — full message history, live presence, and the
ability to post their own messages alongside anything Murph says.
Rooms stay inside your own account. Only people who already belong
to your account can open a shared room link — there's no
cross-account sharing, and no way for someone outside your account to see or
join.
## Buyer-agent participants
Buyer-agent participants are currently available only to accounts enrolled in
the `buyer-agent-chat` rollout. An organization member can add an active buyer
agent to a shared room. A room supports up to five buyer-agent participants.
After Murph replies, each active participant receives the bounded room
transcript and can add a text-only reply attributed to that agent. These
replies are room messages, not Murph assistant turns, and they are not replayed
as assistant messages in later Murph turns. An agent without a conversational
endpoint is shown in the room as unavailable rather than being silently
removed.
Everyone who opens the room sees the same transcript and the same live
updates — no copy-paste, no separate threads.
A conversation starts private to the person who created it. Sharing is an
explicit opt-in, and can be turned off again at any time.
***
## Turning sharing on
Any Murph conversation can become a shared room. From the conversation you
want to share, turn on the **Share** toggle in the Murph chat surface. Murph
gives you a room link shaped like:
```text theme={null}
https://app.apostra.com//murph/c/550e8400-e29b-41d4-a716-446655440000
```
Send that link to a teammate. When they open it, they land in the same
conversation with the full message history already loaded.
Only the person who started a conversation can turn sharing on — a private
conversation isn't visible to anyone else, so there's no one else who could
turn it on. Once a room is shared, any teammate who can open it can turn
sharing off again, which makes the room private to its original owner.
Turning sharing off does not delete anything. The conversation and its
history stay intact — teammates simply lose access until sharing is turned
back on.
***
## Who can join
Shared rooms are scoped to your account. A teammate can open a room
link only if:
* They're signed in to the **same account** as the room's owner.
* The room's sharing toggle is currently on.
There is no mechanism to share a room with someone outside your account,
and no configuration that changes this. If someone from a different
account opens your room link, they get the same "not found" response
as anyone else who isn't allowed to see it — Murph never reveals that the room
exists.
***
## Presence, mentions, and room messages
Once a room is shared, it behaves like a live chat rather than a static
transcript.
**Presence.** Everyone currently viewing the room sees who else has it open,
with each teammate's name and avatar. Presence updates live as people open
and close the room — there's no need to refresh.
**Two ways to send a message.** The composer has two targets:
* **Ask Murph** — sends your message to Murph, the same as any other Murph
turn. Murph may respond, and your message is tagged with your name so
teammates can see who asked.
* **Message room** — posts a message to everyone currently viewing the room,
without asking Murph anything. Use this to talk to teammates in the room
without triggering a Murph response.
You can switch the default target for the whole conversation, or override it
for a single message with a leading mention:
| You type | Where it goes |
| - | - |
| `@Murph can you check this pacing note?` | To Murph, regardless of the current default target |
| `@room heads up, I fixed the budget` | To everyone in the room, not to Murph |
| `@inez can you take a look?` | To the room, with `@inez` kept in the message text |
While a room is open, the composer suggests `@Murph`, `@room`, and the names
of teammates in your account as you type `@`, so you don't need to
remember exact names.
Messages sent after sharing was turned on show the sender's name and avatar.
Older messages that predate this attribution show as a generic **Teammate**.
***
## Finding your rooms
Your Murph conversation list separates rooms into two groups:
* **Live chats** — shared rooms that currently have an active viewer,
including you if you have the room open, with avatars showing who's active.
* **Recents** — everything else: private conversations, and shared rooms
nobody currently has open.
The list refreshes automatically, so a conversation moves into **Live chats**
as soon as a teammate opens it.
Recents follows the scope you are working in: an account, advertiser, or buyer;
switching a rail workspace begins a new chat scope.
The rail shows the five most recent conversations for that scope; choose
**All** to expand the rest. A chat is named after the first thing you ask (or
the action that opened it), never after Murph's response. An empty shared room
does not appear in Recents and is archived 30 minutes after its session ends if
nobody sends a message.
***
## FAQ
No. Shared rooms are limited to people signed in to your own
account. There's no external sharing option.
Teammates who open the room see the full existing history, not just
messages sent after sharing was turned on.
No. Anyone with room access can read the transcript and add new messages,
but existing messages aren't editable by other participants.
No. A room message (`@room` or **Message room**) doesn't invoke Murph, so
it doesn't count as a Murph turn. Only messages sent to Murph do.
Yes, if you're the conversation's owner. Turning sharing off just makes
the room private again — the conversation and its history are
unaffected. Only the original owner can turn sharing back on, since a
private conversation isn't visible to anyone else.
Toggle sharing off from the room's share control. The room becomes
private again and the link stops working for others; history is kept.
***
## Related
The full Murph assistant — setup, merchandising, analytics, and diagnostics.
The underlying endpoints for sharing, presence, and room messages.
# Release Updates
Source: https://docs.apostra.com/v2/features/release-notifications
Catch up on changes relevant to your account, explore capabilities you may not use yet, and browse the release feed
Apostra keeps one account-aware release feed across supported product and agent interfaces. Ask Murph **“What changed since I was last here?”** for a short summary tailored to your account, use the Release notes tool from a compatible MCP host, or open **Release notes** from the web Help menu to browse the feed. The web application is one way to read it, not a requirement that every user log in there.
## The account-aware feed
Release notes has three views:
* **For you** — changes for your buyer or seller role, plus feature-specific changes when Apostra can verify that your account configured or was affected by that feature.
* **Explore** — recent changes to ungated features that fit your role but are not known to be part of your current setup. Explore is a discovery aid, not a guarantee that every feature fits your account.
* **Recent updates** — recent customer-facing entries in the feed for your role.
Apostra remembers how far each user has read within each account. When relevant updates arrive later, Murph can offer a **“What changed since I was last here?”** starter prompt with the unread count. The first visit establishes the starting point instead of presenting old releases as unread.
Updates that arrived since that position are marked **New**. Opening **For you** is what moves your position forward: it advances your read marker to the newest update that view returned, for every view. Browsing **Explore** or **Recent updates** does not move it, so nothing is marked read because you scrolled past it somewhere else.
### How each update is labelled
Every update in the feed carries the same three kinds of label, so a list can be scanned without reading each entry in full:
* **Significance** — how much the change is likely to matter: **Fix**, **Improvement**, **Feature**, or **Headline**. This is a classification, not a severity: a `Fix` is not an alarm and a `Headline` is the one worth stopping for.
* **Maturity** — shown only when it changes how to read the update: **Alpha** and **Beta** mark a capability that is still changing, and **Retired** marks one that is being wound down. A generally available feature carries no maturity label.
* **Why it is in your list** — **Changed for you** when Apostra verified your account was affected, **In your setup** when it verified you have the feature configured, and **For all accounts** when the change applies to everyone in your role. An update never claims to be about your account unless Apostra could verify that.
Each feature update leads with the practical outcome and links to its documentation when available. The Murph action adapts to the update: **See how it fits** for an Explore item, **What should I do?** for a change in your setup, or **Explain this update** in release history. The selected card shows Murph's explanation or retry state inline through the shared typed tool; the action does not add a chat turn or change your composer.
## Shared Slack notes
If you have a shared Slack channel with Apostra, Apostra may also post a short direct follow-up when a reviewed change specifically affects your account. Slack is not the default release feed. These messages are written for the affected setup rather than copied from a technical changelog:
> 🚀 **Apostra v4.9.0** is live - here's what's new for you:
>
> • If your ad-server adapter sync was failing with credential errors, that's fixed — your next sync should go through. Nothing to change on your side.
>
> Any questions? Just ask me here 💬
Updates in the release feed can also include a small learning package:
* **Read guide** opens the canonical Apostra documentation at the most relevant section when one is available.
* **Open feature** appears when the feature has a stable in-product destination and takes you directly there.
* **Open in Murph** appears for shared chat surfaces and opens the registered surface immediately, without drafting a chat message.
* **Show me** appears when Product Commercialization has published a guided lesson. It opens the feature and teaches it step by step in place.
* A screenshot may appear when the feature owns a current, customer-safe image in the documentation. The guide remains the source of truth; the image is never the only instruction.
Not every note needs every action. Fixes generally need only a concise explanation, while new capabilities are expected to carry a guide and a way to start learning them.
## How Slack changes are selected
Slack follow-ups are intentionally rare and can differ by customer:
* **Specific impact** — the follow-up identifies the affected account cohort and why the change applies; a broad buyer/seller label is not enough.
* **Reviewed targeting** — the reason, customer message, and intended cohort require an attributable approval before delivery.
* **Fail closed** — if Apostra cannot resolve the intended cohort safely, the message is withheld rather than widened to more customers.
* **No news, no note** — routine fixes, improvements, and feature releases remain available in the account-aware feed without generating another Slack interruption.
## Asking about any change
Ask Murph in the product or reply in your shared channel. Murph can explain a change, open the release feed, walk you through using something new, or connect you with the team.
When you choose **Open in Murph**, the link carries a registered feature identifier rather than arbitrary prompt text. Murph validates that identifier and opens its registered product surface immediately. The link cannot select an arbitrary tool, and opening the surface does not change your account; actions inside it still follow their normal safeguards.
**Open in Murph** is a product launch, not an interactive tutorial. **Show me** is the teaching experience: it opens an authored guided lesson, highlights the relevant controls, and links back to the full guide. The feature's **?** button restarts the same lesson later. Features without a reviewed lesson do not show that action.
## Adjusting or stopping the notes
Periodic **Product Updates** in Slack include an **Unsubscribe** button. A member of the connected Apostra account can use it to stop future Product Updates in that channel without removing the account's release history or muting another connected channel.
You can also ask Murph to show or change the Product Updates setting for your
account's primary Slack channel. Product Updates currently have a fixed
schedule, so the setting is on/off rather than a frequency selector.
Product Updates and direct follow-up are separate. Unsubscribing from the periodic digest does not suppress a separately reviewed message about a change that specifically affects your account or requires action. Tell us in your shared channel if you want that follow-up policy adjusted too. Your account-aware history remains available through supported product and agent interfaces, and the complete public record is always in the [changelog](/v2/changelog).
# Sandbox Mode
Source: https://docs.apostra.com/v2/features/sandbox
Run supported tests with a reviewed sandbox account
## Overview
Sandbox mode supports the platform's no-spend test workflows when they use a
sandbox account and a reviewed provider test context. It does not make every
provider account or every execution request non-deliverable on its own.
Validate supported workflows with a sandbox account and the provider test context they require.
Supported account resolution and synchronization retain the sandbox setting for the selected account.
***
## How It Works
Sandbox is **account-level, not per-request**. Supported account resolution and
synchronization retain the sandbox setting for the selected account. Keep a
dedicated sandbox account separate from production accounts.
When you create an advertiser with `sandbox: true`, supported platform account
resolution and synchronization use sandbox context. A request that names an
`account_id` relies on that account's own sandbox semantics. The advertiser
flag alone does not prove that an arbitrary provider account cannot reserve,
deliver, or otherwise act on a request.
Use the provider's documented test account and the reviewed provider test
context before executing a test. The platform preserves its sandbox context for
supported flows; it does not replace the provider's account controls.
### Seller ad-server setup
For an ad-server-backed storefront, Apostra also needs a dedicated advertiser/account
inside the seller's ad server. Keep it separate from every production advertiser. The
seller can either:
* Create or designate a sandbox advertiser/account and assign it to Apostra
service account, then map it as the storefront's sandbox advertiser; or
* Grant Apostra service account permission to create advertisers so Apostra
can provision `Apostra - Sandbox` automatically.
Until one of those paths is complete, Apostra will recommend the setup in storefront
readiness and may send a reviewed seller Nudge. This recommendation does not block live
selling, but smoke tests will not run through a production or default advertiser as a
fallback.
For protocol-level details on how sandbox mode works, see the [AdCP Sandbox documentation](https://docs.adcontextprotocol.org/docs/media-buy/advanced-topics/sandbox#sandbox-mode).
***
## Creating a Sandbox Advertiser
### Via API
Set `sandbox: true` in the create advertiser request body:
```bash curl theme={null}
curl -X POST "https://api.apostra.com/api/v2/buyer/advertisers" \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp (Sandbox)",
"description": "Integration testing advertiser",
"sandbox": true
}'
```
```typescript TypeScript theme={null}
const response = await fetch('https://api.apostra.com/api/v2/buyer/advertisers', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SCOPE3_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Acme Corp (Sandbox)',
description: 'Integration testing advertiser',
sandbox: true,
}),
})
const { data } = await response.json()
console.log('Sandbox advertiser:', data.id, data.sandbox) // sandbox: true
```
```python Python theme={null}
import requests, os
response = requests.post(
'https://api.apostra.com/api/v2/buyer/advertisers',
headers={
'Authorization': f'Bearer {os.environ["SCOPE3_API_KEY"]}',
'Content-Type': 'application/json',
},
json={
'name': 'Acme Corp (Sandbox)',
'description': 'Integration testing advertiser',
'sandbox': True,
},
)
advertiser = response.json()['data']
print('Sandbox advertiser:', advertiser['id'], advertiser['sandbox'])
```
**Response:**
```json theme={null}
{
"data": {
"id": "adv_abc123",
"name": "Acme Corp (Sandbox)",
"description": "Integration testing advertiser",
"status": "active",
"sandbox": true,
"createdAt": "2026-02-22T21:35:44Z",
"updatedAt": "2026-02-22T21:35:44Z",
"linkedBrand": null,
"brand": null,
"brandWarning": null
}
}
```
### Via UI
When creating an advertiser in the dashboard, toggle the **Sandbox** switch before saving. Sandbox advertisers are shown with a badge in the advertiser list for easy identification.
**Sandbox is permanent.** Once an advertiser is created with `sandbox: true`, the flag cannot be changed. This protects against accidentally switching an advertiser from sandbox to production after campaigns have been configured.
***
## Using Sandbox
Supported sandbox workflows use the same API endpoints as production. Before
executing one, confirm that the selected account is a sandbox account and that
the provider's reviewed test context supports the operation you intend to run.
For example, executing a campaign:
```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/buyer/campaigns/{campaignId}/execute" \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json"
```
The platform keeps sandbox context during supported account resolution and
synchronization. If this request names an `account_id`, the provider action
still relies on that account's sandbox semantics and the provider's reviewed
test context. A sandbox advertiser flag by itself does not prove that every
provider response is simulated or that an arbitrary provider account cannot
reserve, deliver, or spend.
### Media Company V3 preview
Every Media Company can create a
sandbox Advertiser, Campaign, Creative, Proposal request, and staged MediaBuy
through `/mcp/v3` without switching to a Buyer account. The preview exposes
`save_advertiser`, `save_campaign`, `save_creative`,
`save_creative_collection`, `request_proposals`, and `save_media_buy` in the
Seller account.
No entitlement or beta grant is required for this no-spend workflow. Live
Media Company-managed campaigns use the same tools but are a separate,
customer-scoped rollout controlled by the `amc-campaign-management` flag.
Live operations retain normal approval, creative, financial, publisher, and
inventory-source readiness checks; the flag does not bypass them. Giving
clients access to operate their own campaigns is separate and uses the
`storefront-self-service-buyers` rollout. Neither flag expands an Advertiser's
durable Storefront scope.
This path is deliberately confined:
* `save_advertiser` creates a no-spend Advertiser with `sandbox: true`; when the
live rollout is enabled for the organization, omitting `sandbox` creates a
live Advertiser;
* the advertiser and campaign remain owned by the authenticated Media Company;
* Campaign creation is pinned server-side to the company's own Storefront,
even when `sellerIds` is omitted; and
* Proposal requests can address only that Storefront;
* Proposal acceptance and direct MediaBuy staging reject foreign qualified
Proposal and Product IDs; and
* existing MediaBuy updates require every line item to resolve durably to that
Storefront.
Integrated buyer `search` and `get` can inspect an authorized sandbox or live
Advertiser, Campaign, Creative, Proposal, and MediaBuy. Opening the company's
own Seller with `include: ["products"]` returns its wholesale Products,
including any eligible `signal_targeting_options`; pass a selected Signal
through that Product's `targetingOverlay` when staging the MediaBuy. After launch,
`get_delivery({ report: "live_campaign_delivery", ... })` reads bounded delivery
live for one exact own-supply campaign. It accepts the campaign ID and date
range, not warehouse metrics, dimensions, other filters, or pagination. The
stored aggregate contract remains available as `campaign_delivery`.
Seller Accounts also get a route-backed organization workspace
selector. Inventory keeps the existing seller experience; Campaigns lists the
account's sandbox Advertisers, plus its live own-supply Advertisers once the
account is enrolled in `amc-campaign-management`, and opens the shared
Campaigns and Creative Pages. The native roster read stays fail-closed to
sandbox for an unenrolled account; it does not grant general Buyer REST
access. Two additional read-only compatibility calls hydrate those shared
Pages: Campaign listing requires an explicit, durably bound `advertiserId` in
either environment and removes Campaigns outside own supply; the promoted
Creative list requires the same bound Advertiser and exposes no attach or
generation actions. Advertiser, Campaign, and Creative detail, mutations, and
reporting remain V3-only for the Seller Account. Agents appears in the same
selector when the organization separately has Agent workspace access. Within a
browser session, returning to Campaigns restores the last Advertiser that was
successfully resolved from that roster; an invalid or foreign Advertiser ID
returns to account scope instead.
Creating an Advertiser or requesting Reporting from the Campaigns workspace
stages a Murph request, so the guarded V3 flow remains the only write and
delivery path — the workspace's "+ Add advertiser" control stages a
sandbox-creation prompt today; an enrolled account can ask Murph directly to
create a live Advertiser. Marketplace, Connections, Buyer Setup, and unbound
production Advertisers remain outside this preview.
Independent Buyer Accounts and their existing sandbox workflows are unchanged.
***
## Validate an Agent in sandbox
Agent validation now uses the Agent's public `validationSkill` and the ordinary
V3 Buyer workflow. Resolve the exact Agent with `get({ kind: "agent" })`, select
a profile from its returned plan, fetch the versioned skill, and follow that
skill without substituting legacy test wrappers or generic V2 `api_call`
orchestration.
Run the profile with sandbox inputs first. The workflow uses the same typed V3
objects and operations a Buyer uses: Advertiser, Campaign, Creative, Proposal,
MediaBuy, and bounded delivery reads. It preserves normal confirmation before
mutations and never treats a simulated run as certification.
After the run or an intentional stop, read the same Agent with
`include: ["validationRuns", "diagnostics"]`. Pass an exact
`validationRunId` to inspect its bounded trace. If the run selected Products
from a Source, open that Source's diagnostics as well. Report which assertions
were observed, failed, unavailable, or unexercised, and report cleanup
separately.
Historical Murph test-run records remain readable during the bounded rollback
window, but they are no longer a launch surface or readiness authority. Agent
validation evidence and Source diagnostics are the canonical history.
***
## Filtering Sandbox Advertisers
The `sandbox` field is returned on every advertiser response. Use the optional `sandbox` query parameter to filter:
```bash theme={null}
# List only sandbox advertisers
curl "https://api.apostra.com/api/v2/buyer/advertisers?sandbox=true" \
-H "Authorization: Bearer your-api-key"
# List only production advertisers
curl "https://api.apostra.com/api/v2/buyer/advertisers?sandbox=false" \
-H "Authorization: Bearer your-api-key"
```
In the dashboard, sandbox advertisers are shown with a **Sandbox** badge so they are easy to distinguish from production advertisers at a glance.
***
## Key Constraints
| Constraint | Detail |
| - | - |
| **Immutable flag** | `sandbox` cannot be changed after advertiser creation |
| **Strict isolation** | Sandbox and production data are fully isolated — no risk of mixing test and real traffic |
| **No silent fallback** | If sandbox execution cannot proceed, the API returns an explicit error rather than falling back to production |
***
## Next Steps
Full schema for `POST /advertisers`, including the `sandbox` field.
Protocol-level details on how sandbox mode works in AdCP.
Get up and running with Apostra API.
# Typed Storefront Tools
Source: https://docs.apostra.com/v2/features/storefront-typed-tools
Give Murph the exact request shape of every storefront operation before it acts
Typed storefront tools are enabled automatically in eligible private seller
conversations. No feature-flag enrollment is required.
## Overview
When you ask [Murph](/v2/setup/ask-murph) to operate your storefront — set selling
terms, author a product, update operating instructions — Murph translates your
request into an API call. By default it does this through a single general-purpose
tool and reasons out the request shape as it goes.
**Typed storefront tools** change that. In an eligible private seller
conversation, each covered storefront operation is presented to Murph as its
*own* tool, carrying that operation's exact request schema. Murph sees the
precise fields an operation expects **before** it makes the call, instead of
inferring them.
## What it changes for you
* **Fewer failed operations.** Most tool-call errors come from the assistant
guessing a field name or type. When the schema is attached to the tool, that
class of error largely disappears — the request is shaped correctly the first
time.
* **Same conversation.** Nothing changes in how you talk to Murph. You ask for the
same things in the same way; the reliability improvement happens underneath.
* **Full coverage preserved.** Operations that don't yet have a typed tool still
work through the general-purpose path, so nothing you could do before stops
working.
The storefront tool catalog is generally available independently of
[typed buyer tools](/v2/features/buyer-typed-tools), which have their own
availability policy.
Typed tools presented to Murph are distinct from the host-only aliases used by
interactive Pages. The Teach Page uses `teach_search_materials`,
`teach_get_material`, `teach_save_material`, and `teach_renew_capability` 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. The Library Page uses its own host-only `library_*`
aliases in the same way; see
[MCP Apps](/v2/reference/mcp-apps#page-capability-and-tool-bridge).
## Availability
The typed catalog is available automatically when Murph has an authenticated,
private seller context. Shared channels retain a narrower tool surface to avoid
exposing customer-specific operations, and explicitly configured evaluation
sessions continue to use their supplied tool set.
## Related
The assistant these tools run behind.
One of the storefront operations Murph runs for you.
# Amazon Ads compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/amazon
What you can and cannot do with Amazon Ads through Apostra.
This page compares the Amazon Ads capabilities represented in the
Apostra adapter with what Apostra currently makes available to buyers.
It answers a practical question: **what can I do through Apostra today, and
what would still require another path?**
**Last reviewed 2026-08-29.** This is Apostra's description of
its own adapter, based on the implementation and the linked provider
documentation. Amazon Ads has not reviewed or endorsed this page.
Availability can still vary by account permissions, market, and provider access.
## At a glance
| Area | 🟢 Available | 🟡 Partial | 🔵 Testing | ⚪ Planned |
| - | - | - | - | - |
| Campaign products | **0** | **0** | **1** | **8** |
| Creative formats | **0** | **0** | **0** | **11** |
| Adapter capabilities | **0** | **0** | **2** | **14** |
### How to read availability
* 🟢 **Available** — Offered through Apostra now, subject to your ad account's permissions and eligibility with the provider.
* 🟡 **Partial** — Some of the workflow is available; the Comments column explains the boundary.
* 🔵 **Testing** — Apostra is validating the workflow before offering it broadly.
* ⚪ **Planned** — Not offered through Apostra yet. The provider may still support it directly; Comments explains why and links a roadmap reference.
## Roadmap
The adapter roadmap is tracked in AI-1953 (a plain-text reference — Apostra's internal issue tracker is not public).
This roadmap tracks Amazon Ads product completeness, account access, and live readiness. Every row that is not currently available shows
that reference or a more specific one in its References column, so customers
and partners can quote a stable identifier when asking about a capability.
## Connection, accounts, and identity
Amazon Ads connects to Apostra through an OAuth grant. Connecting authorizes Apostra to reach your Sponsored Ads profile or DSP advertiser (two separate systems under one login), which you then map to your advertisers. The mapped account decides which Amazon Ads tenant a buy runs on: a media buy for an unmapped advertiser fails closed with `account_mapping_required` rather than silently using another account. Connection, account-discovery, and revocation behavior is the same across ad platforms — see [Platform accounts and advertiser mapping](/v2/concepts/ad-platform-accounts) for the full model and the [Amazon Ads section of the connecting guide](/v2/guides/connecting-ad-platforms#amazon-ads) for Amazon Ads-specific permissions.
| Apostra concept | Amazon Ads concept | What it represents |
| - | - | - |
| Account (`account`) | provider advertising account | The authenticated selected account is the execution boundary. |
| Media Buy (`media_buy`) | campaign | Media Buy maps to campaign. |
| Package (`package`) | ad group | Budget, targeting, optimization, and flight bindings. |
| Creative Assignment (`creative_assignment`) | ad or creative | Creative materialization or assignment surface declared by current code. |
## Campaign products
| Feature | Availability | References | Comments |
| - | - | - | - |
| Sp Manual | 🔵 Testing | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Amazon Sponsored Products Manual creation is currently limited to explicitly enrolled internal test buyers. Account readiness and terminal recovery remain available when new creates are turned off. Pilot, eligible-buyer, and GA access remain future stages with no committed date. Current boundary: Apostra implementation/testing. |
| Sp Auto | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
| Sb Awareness | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
| Sb Video | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
| Sd Retargeting | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
| Sd Conquest | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
| Dsp Display | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
| Dsp Video | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
| Dsp Audio | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
## Creative formats
These rows describe **Apostra availability**, not a claim that the ad platform
itself prohibits an unavailable format.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Sp Product Ad | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Sb Product Collection | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Sb Store Spotlight | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Sb Video | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Sd Product Targeting | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Sd Audiences | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Sd Video | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Dsp Display | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Dsp Video | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Dsp Audio | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Dsp Responsive Ecommerce | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
## What the adapter can do
| Feature | Availability | References | Comments |
| - | - | - | - |
| List Accounts | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Accounts | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Account synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Products | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Creatives | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| List Creatives | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Preview Creative | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Amazon has product-specific preview APIs, but Apostra does not yet implement their selected-account and creative-identity mapping or safe handling of provider markup. Unsupported product families fail closed. Current boundary: Apostra implementation/testing. |
| Create Media Buy | 🔵 Testing | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Amazon Sponsored Products Manual creation is currently limited to explicitly enrolled internal test buyers. Pilot, eligible-buyer, and GA access are future stages with no committed date. This capability does not enable creation for other Amazon product families. Current boundary: Apostra implementation/testing. |
| Get Media Buys | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra uses an exact-account Sponsored Products Manual campaign and ad-group read during controlled identity and post-update verification. This read does not enable broader Amazon product families or general availability. Current boundary: Apostra implementation/testing. |
| Update Media Buy | 🔵 Testing | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | An exact existing Amazon Sponsored Products Manual buy can be updated or canceled through its stored account and campaign identity, including when new creates are turned off. This recovery path does not make other Amazon product families generally available. Current boundary: Apostra implementation/testing. |
| Get Media Buy Delivery | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Signals | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Signal discovery is not implemented. Current boundary: Apostra implementation/testing. |
| Sync Audiences | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Audience synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Sync Event Sources | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Log Event | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Event ingestion is not implemented. Current boundary: Apostra implementation/testing. |
| Sync Catalogs | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Account Financials | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
## Placement choices
Apostra does not expose a separate Amazon Ads placement selector.
## Targeting
| Feature | Availability | References | Comments |
| - | - | - | - |
| Keyword Targets | 🔵 Testing | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra is still testing this targeting choice before making it generally available. Current boundary: Apostra implementation/testing. |
| Match Types | 🔵 Testing | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra is still testing this targeting choice before making it generally available. Current boundary: Apostra implementation/testing. |
| Product Codes | 🔵 Testing | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Apostra is still testing this targeting choice before making it generally available. Current boundary: Apostra implementation/testing. |
## Reporting
Apostra reports Amazon Ads delivery day by day (there is no hourly delivery breakdown), aggregated nightly from adapter-reported delivery, in the advertiser's home currency using the booked or historical exchange rate — any cross-currency rollup is always labeled an estimate and is never used for billing. See [Reporting overview](/v2/guides/reporting-overview) for the full model. Apostra does not currently publish an attribution-finality window for Amazon Ads; the table below lists every metric the adapter surfaces today.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Impressions | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Campaign totals |
| Spend | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Account-currency amount |
| Clicks | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Campaign totals |
| Conversions | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Attributed purchases |
| Conversion Value | ⚪ Planned | [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us) · AI-1953 | Attributed sales amount |
## Webhooks and async lifecycle
Amazon Ads does not push status, delivery, or reporting events into Apostra — every campaign product declares `supports_webhooks: false`.
* **Status.** Apostra does not currently poll Amazon Ads in the background for creative or media-buy status changes; status reflects what the adapter returned on your last call.
* **Delivery.** Apostra has not yet made on-demand delivery fetching generally available for Amazon Ads — see Get Media Buy Delivery in "What the adapter can do" above. The `/reporting/metrics` endpoints never call the adapter either way — they read from a separately-populated store (see Reporting below), so a reporting request does not itself refresh provider delivery.
This is separate from Apostra's own buyer webhook subscriptions — an unrelated account-level event feed (currently `discovery.revision` only), documented in [Buyer webhooks](/v2/buyer/webhooks).
## Market and account availability
Apostra does not promise that every feature is enabled for every account or
market. Availability is checked against your selected ad account rather than a global market list on this page.
## Important limitations
* **Apostra implementation/testing.** The listed campaign products exist in the current adapter code but are still being certified before general availability.
* **Apostra implementation/testing.** Creative formats recognized by the adapter are not available unless their row is explicitly marked Available.
* **the ad platform's API or your account's eligibility.** Amazon account permissions, inventory access, and product eligibility determine what can run for a selected account.
## Technical mapping
Engineering-level detail — raw provider objectives, billing enums, optimization
codes, and object-field mappings. Not required reading to understand what
Apostra can do with Amazon Ads; see the tables above for that.
| Product | Provider objective | Optimization | Billing |
| - | - | - | - |
| Sp Manual (`amazon_sp_manual`) | `sponsoredProducts` | `manual` | `legacyForSales` |
| Sp Auto (`amazon_sp_auto`) | `sponsoredProducts` | `auto` | `autoForSales` |
| Sb Awareness (`amazon_sb_awareness`) | `sponsoredBrands` | `manual` | `legacyForSales` |
| Sb Video (`amazon_sb_video`) | `sponsoredBrands` | `manual` | `legacyForSales` |
| Sd Retargeting (`amazon_sd_retargeting`) | `sponsoredDisplay` | `manual` | `legacyForSales` |
| Sd Conquest (`amazon_sd_conquest`) | `sponsoredDisplay` | `manual` | `legacyForSales` |
| Dsp Display (`amazon_dsp_display`) | `dsp` | `manual` | `manual` |
| Dsp Video (`amazon_dsp_video`) | `dsp` | `manual` | `manual` |
| Dsp Audio (`amazon_dsp_audio`) | `dsp` | `manual` | `manual` |
| Format | Amazon Ads shape |
| - | - |
| Sp Product Ad (`amazon_sp_product_ad`) | Canonical Code Format |
| Sb Product Collection (`amazon_sb_product_collection`) | Canonical Code Format |
| Sb Store Spotlight (`amazon_sb_store_spotlight`) | Canonical Code Format |
| Sb Video (`amazon_sb_video`) | Canonical Code Format |
| Sd Product Targeting (`amazon_sd_product_targeting`) | Canonical Code Format |
| Sd Audiences (`amazon_sd_audiences`) | Canonical Code Format |
| Sd Video (`amazon_sd_video`) | Canonical Code Format |
| Dsp Display (`amazon_dsp_display`) | Canonical Code Format |
| Dsp Video (`amazon_dsp_video`) | Canonical Code Format |
| Dsp Audio (`amazon_dsp_audio`) | Canonical Code Format |
| Dsp Responsive Ecommerce (`amazon_dsp_responsive_ecommerce`) | Canonical Code Format |
| Targeting choice | Provider field |
| - | - |
| Keyword Targets (`keyword_targets`) | `Amazon Sponsored Products Manual maps each canonical keyword and match type to the exact selected ad group; unsupported or empty targets fail before campaign creation.` |
| Match Types (`match_types`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Product Codes (`product_codes`) | `Amazon Sponsored Products Manual maps canonical product codes to ASIN product ads; the selected account and marketplace remain authoritative for eligibility.` |
| Apostra output | Amazon Ads source |
| - | - |
| Impressions (`impressions`) | `impressions` |
| Spend (`spend`) | `cost` |
| Clicks (`clicks`) | `clicks` |
| Conversions (`conversions`) | `purchases7d \| purchases` |
| Conversion Value (`conversion_value`) | `sales7d \| sales` |
## Provider documentation
* [Amazon Ads public documentation](https://advertising.amazon.com/API/docs/en-us)
* [Amazon DSP preview reference](https://advertising.amazon.com/API/docs/en-us/guides/dsp/creative-management)
* [Amazon DSP preview schema](https://d1y2lf8k3vrkfu.cloudfront.net/openapi/en-us/dest/DSPAdCreativeAPI-V1_prod_3p.json)
* [Amazon Sponsored Display preview reference](https://advertising.amazon.com/API/docs/en-us/guides/sponsored-display/creatives)
* [Amazon Sponsored Display preview schema](https://d3a0d0y2hgofx6.cloudfront.net/en-us/sponsored-display/3-0/openapi.yaml)
* [Amazon Sponsored TV preview reference](https://advertising.amazon.com/API/docs/en-us/guides/sponsored-tv/creatives/creative-preview)
# AudioStack creative-tool compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/audiostack-creative
Current public support and limitations for AudioStack audio generation in Apostra
AudioStack is a creative-only audio generation service. It is not a direct
media-buy adapter. Apostra uses it to turn a script and voice settings into one
rendered `audio_hosted` creative manifest.
**Last reviewed 2026-09-13.** This page describes what Apostra
integration offers. AudioStack has not reviewed or endorsed it. Availability
can still vary by account, voice, language, and provider access.
## Roadmap
The AudioStack integration roadmap is tracked as AI-6601 (a plain-text
reference — Apostra's internal issue tracker is not public). The issue
covers creative discovery, reusable-library and multi-output gaps, and live
validation. The issue key remains a stable reference even for readers who
cannot access the internal Linear workspace.
## Connection, accounts, and identity
AudioStack connects to Apostra through an API key, not OAuth: the key is an
access grant billed to one authorized organization. There is no ad account or
manager-account hierarchy to discover. The advertiser you map to that
connection chooses which AudioStack organization is billed for a generation;
your brand's `voice_synthesis` setting chooses the voice. Mapping and fail-closed
behavior otherwise follow the same model as ad platforms — see
[Platform accounts and advertiser mapping](/v2/concepts/ad-platform-accounts).
AudioStack does not yet have a dedicated section in the connecting guide.
## What you can do through Apostra
| Feature | Availability | References | Comments |
| - | - | - | - |
| Build an audio creative (`build_creative`) | 🟢 Available | [AudioStack API overview](https://docs.audiostack.ai/docs/audiostack-api) | One request performs a full AudioStack render and returns one `audio_hosted` creative containing the rendered audio. |
| Preview without rendering (`preview_creative`) | ⚪ Planned | [AudioStack API overview](https://docs.audiostack.ai/docs/audiostack-api) · AI-6601 | To audition an output, run a full `build_creative` render and play the returned audio; there is no preview-only path. Current boundary: Apostra implementation/testing. |
| Reuse an AudioStack library creative | ⚪ Planned | [AudioStack API overview](https://docs.audiostack.ai/docs/audiostack-api) · AI-6601 | The integration does not look up, sync, list, or read back creatives in an AudioStack library. Each build starts a new render. Current boundary: Apostra implementation/testing. |
| Produce several lengths in one request | ⚪ Planned | [AudioStack API overview](https://docs.audiostack.ai/docs/audiostack-api) · AI-6601 | One build selects one duration. Submit separate builds for 15-second and 30-second versions. Current boundary: Apostra implementation/testing. |
| Buy media | ⚪ Planned | — | AudioStack is used for creative generation, not products, campaigns, targeting, or delivery reporting. This is outside the integration's scope rather than a gap on the roadmap above. |
## Supported build shape
AudioStack accepts the canonical `audio_hosted` format. A build requires a text
`script` asset and can bind a voice, language, brand name, speed, and music
template. The current runtime supports one of these output choices per call:
* `duration_ms_exact: 15000` for a 15-second audio ad;
* `duration_ms_exact: 30000` for a 30-second audio ad; or
* no exact duration for the voiceover path.
Voiceovers use AudioStack's Audioform service at the script's natural spoken
length, without background music. They do not use the deprecated script and
speech endpoints.
Legacy agent-URL format references are not accepted. AudioStack does not
publish a separate buyer-selectable format catalog; `list_creative_formats`
returns an empty list because the integration consumes canonical
`audio_hosted` directly.
## Webhooks and async lifecycle
Not applicable. `build_creative` is synchronous: one request performs the full
render and returns the result, so there is no async task and no
webhook/polling distinction to make here — unlike the direct ad-platform
adapters, which use polling (see each adapter's own "Webhooks and async
lifecycle" section).
## Reporting
Not applicable. AudioStack is a creative-only integration; it does not report
delivery, spend, or performance metrics. See
[Reporting overview](/v2/guides/reporting-overview) for how ad-platform
delivery reporting works.
## Provider and account constraints
AudioStack access is bring-your-own-key. The connected credential resolves one
billable AudioStack organization, and the advertiser must be mapped to that
provider connection before generation. Authentication, provider rate limits,
voice availability, and upstream rendering availability can still block an
otherwise valid build. Apostra does not publish a market or voice
availability list for AudioStack; the selected provider account determines what
is available at request time.
## Provider documentation
* [AudioStack API overview](https://docs.audiostack.ai/docs/audiostack-api)
* [Generative creative provider credentials](/v2/buyer/creatives/generative-creative)
# Creative preview support
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/creative-preview
See which ad platforms provide authoritative creative previews and when Apostra fails closed.
Apostra uses a platform's own preview when one is available. These previews
show the platform's supported preview surface; when the provider addresses
placements, Apostra maps them exactly. They do not guarantee approval or
delivery.
Buyer adapter preview links are provider-hosted and may expire. Those adapter
surfaces return links and never execute provider embed code. In seller
creative review, a publisher-designated `preview_creative` route may return
HTML or a URL. Apostra preserves its authority, fidelity, and expiry, but
does not mount those provider-controlled bytes in the reviewer's browser.
Submitted creative tags, VAST, trackers, and other source markup likewise
remain inert text. When a safe image thumbnail is available on buyer adapter
surfaces, Apostra can show the actual creative inline.
## Support by platform
| Platform | Available through Apostra | Provider boundary | Current behavior |
| - | - | - | - |
| Meta Ads | Yes | Saved creatives and transient image specifications. | Apostra previews eligible Facebook and Instagram placements. Saved creatives must belong to your selected ad account. |
| TikTok Ads | Saved video only | Saved Asset Library video with an authored CTA. | Apostra previews an already-synced `tiktok_in_feed_video` after selected-account ledger and video readback. Other TikTok formats and unsynchronized or transient inputs fail closed. |
| Pinterest Ads | Yes | Expiring shareable previews for saved ads in one selected ad group. | Apostra verifies one saved assignment and preserves Pinterest's URL and expiry. Unsynchronized creatives do not use a fallback. |
| Snap Ads | Yes | Hosted links for saved 9:16 image and video creatives. | Apostra verifies the selected-account creative and media before returning Snap's signed preview. Unsynchronized manifests and explicit placement selections fail closed. |
| Google Ads | Saved Performance Max only | Shareable previews for saved Performance Max asset groups. | Apostra returns one verified Performance Max UI preview from the selected customer. It does not claim placements, dimensions, thumbnails, or transient fallback. |
| OpenAI Ads | Not yet | The Ads prose reference names a 24-hour preview operation, but the published OpenAPI schema defines neither the operation nor its response. | The adapter does not advertise `preview_creative`. The account `preview_url` is a favicon preview, not an ad preview, and no approximation is used. |
| Amazon Ads | Not yet | [Amazon DSP](https://advertising.amazon.com/API/docs/en-us/guides/dsp/creative-management) accepts saved or transient creatives; Sponsored Display accepts transient specifications; Sponsored TV accepts saved creative or verified-asset identities. The APIs return provider markup—HTML, plus VAST 2 or VAST 3 for DSP video—and do not document an artifact expiry. Sponsored Brands and Sponsored Products have no documented preview path. | The adapter does not advertise `preview_creative`. It must implement each product-specific account, identity, format, markup-isolation, and lifetime boundary before support is enabled. |
| LinkedIn Ads | Not yet | The [Ad Preview API](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/ad-preview?view=li-lms-2026-07) supports saved SponsoredCreative URNs and separate transient live-preview actions. Saved preview iframe sources last up to three hours; transient responses use `srcdoc`. | The adapter does not advertise `preview_creative`. A first slice must verify a saved creative under the selected account, return only a validated HTTPS link, preserve device roles, cap expiry at request time plus three hours, and discard provider HTML. |
| Reddit Ads | Not yet | [Reddit previews](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/preview-an-ad) are saved-ad only. The API can refresh an existing link for up to 30 days, but the first link must be generated in Ads Manager and propagation can take up to 20 minutes. | The adapter does not advertise `preview_creative`. A missing initial link, transient creative, account mismatch, or stale expiry fails closed. |
| Spotify Ads | Not yet | Account-scoped [saved-ad reads](https://developer.spotify.com/documentation/ads-api/reference/v3.0/getAd) can expose `AD_PREVIEW_URL`; draft ads do not, and Spotify documents no preview expiry or anonymous-access guarantee. | The adapter does not advertise `preview_creative`. It must prove selected-account identity, URL safety, accessibility, and lifetime without creating an ad solely for preview. |
For the wider unavailable OpenAI boundary, see
[OpenAI Ads](/v2/guides/ad-platform-compatibility/openai-ads).
See each platform's compatibility page for its wider creative and campaign
support.
## Meta preview boundary
Meta preview requests use the ad account selected on Apostra
connection. If the source creative has already been synchronized, Apostra
resolves its Meta creative ID from that advertiser and account's creative
record, then verifies the creative still reads back under the selected account.
Meta preview links are treated as expiring after five minutes; use **Refresh**
to request a new link after that deadline.
For an unsynchronized image manifest, Apostra can submit a read-only
transient specification. Other unsynchronized creative types must be synced
before previewing. A request fails closed when the creative identity, account,
format, or requested placement cannot be verified.
The response contains one or more HTTPS Meta preview links, labeled by
placement. Creatives already synced to Meta may also include a provider-readback
image thumbnail; transient previews remain link-only. Apostra does not
return executable preview HTML.
## TikTok preview boundary
TikTok preview requests use the advertiser account and delegated credential
selected on Apostra connection. Apostra resolves the source
creative through that tenant and account's ledger, then re-reads the exact
TikTok Asset Library video. The video must still be displayable and explicitly
eligible for `PLACEMENT_TIKTOK`.
The provider request uses TikTok's `SINGLE_VIDEO` preview mode with the saved
video ID and authored display name, ad text, and required stored CTA. Existing
creatives that store the historical `DOWNLOAD` value continue to work;
Apostra preserves that authored identity and sends TikTok's current
`DOWNLOAD_NOW` value only on provider requests. The response is one link
labeled with the existing `tiktok_for_you_feed` public placement ID.
That ID maps to the provider's complete `PLACEMENT_TIKTOK` bundle and does not
prove a single feed destination. Apostra conservatively requires a fresh
preview after 24 hours. Provider iframe HTML and adjustment tips are discarded;
no thumbnail fallback is returned.
TikTok's `ADS_CREATION` table documents `SINGLE_IMAGE` as Pangle-only, and its
separate `SINGLE_IMAGE` preview example uses `PLACEMENT_PANGLE`. This adapter
publishes `PLACEMENT_TIKTOK` and has no faithful saved-image mapping for that
preview shape. Carousel, Spark, generated, raw-URL transient, and delivery
variant requests require fields or identities the generic creative manifest
does not faithfully carry. Apostra rejects those shapes instead of
inventing provider fields or substituting a placement.
## Pinterest preview boundary
Pinterest preview requests use the ad account selected on Apostra
connection. The source creative must already be synchronized and resolve to
exactly one completed assignment in that account. Before requesting the
preview, Apostra reads back the selected account, ad group, Pin, and ad from
Pinterest and verifies that they still match the saved source record and each
other. The preview's placement label is the ad group's exact Pinterest
`placement_group` (`ALL`, `BROWSE`, `SEARCH`, or `OTHER`); Apostra does not
substitute another placement.
Apostra then uses Pinterest's
[`campaign_ad_preview`](https://developers.pinterest.com/docs/work-with-ads/generate-shareable-ad-previews/)
surface and returns one credential-free HTTPS Pinterest preview link with the
expiry Pinterest supplied. The result is link-only: it has no executable HTML,
iframe, script, or inline thumbnail. Pinterest creates an expiring preview
record for the existing assignment; this request does not create or change the
campaign, ad group, ad, or Pin. Pinterest classifies this POST under the
`ads:write` OAuth scope even though it does not mutate those campaign objects.
Pinterest batches accept at most four creatives; every accepted item starts in
the four-worker pool under one 30-second aggregate deadline, and a larger batch
is rejected before account, ledger, or provider work.
Unsynchronized manifests do not use Pinterest's transient Pin or image preview
surface because that path cannot preserve the same saved account, assignment,
placement, and provider-expiry identity. Ambiguous assignments, stale or
foreign provider objects, unsupported formats, unsafe URLs, and mismatched
placement requests fail closed. Pinterest has no reference-renderer fallback
through Apostra.
## Snap preview boundary
Snap preview requests use the delegated credential and ad account selected on
Apostra connection. Apostra resolves an already synchronized source
creative through that tenant and account's creative record, then reads the Snap
ad account, creative, and backing media before requesting Snap's hosted preview.
The ordinary supported formats are 9:16 image and video creatives; other or
unsynchronized manifests must be synchronized first.
Snap batches accept at most four creatives; every accepted item starts in the
four-worker pool under one 30-second aggregate deadline. Apostra rejects a
larger batch before reading the account, saved creative record, or Snap API.
Snap's preview endpoint does not accept a transient creative specification or
a placement selector. Apostra therefore rejects every explicit placement
request rather than substituting automatic placement. A successful request
returns one generic 1080×1920 full-screen render. The role describes the hosted
render; it does not claim a campaign placement was selected.
Snap documents its hosted page as requiring no login and remaining available
for 90 days. Apostra returns the provider's exact expiry and only constructs
the external `ad-preview.snapchat.com` link when the signed creative ID, expiry,
and signature agree with Snap's response. It returns no provider HTML or
thumbnail.
## Google Performance Max preview boundary
Google preview requests use the customer account selected on Apostra
connection. The source creative must already be synchronized as
`google_performance_max` and resolve to exactly one completed asset-group
assignment for that tenant, customer, and source. Apostra reads back the
selected non-manager customer, exact Performance Max campaign, and exact saved
asset group before requesting Google's `UI_PREVIEW`. A renamed, removed,
foreign, missing, duplicated, or in-flight identity fails closed.
The response contains one expiring, credential-free HTTPS Google Ads external
preview link. Apostra preserves Google's expiry and returns no provider
HTML, iframe, script, thumbnail, or invented dimensions. Google's aggregate
Performance Max UI preview does not expose a placement selector, so any
requested placement is rejected; Apostra does not label the page as a
Search, Display, YouTube, Gmail, Maps, or Discover placement.
Unsynchronized or transient input, YouTube, responsive search, responsive
display, delivery variants, unsafe URLs, and response identity drift are not
approximated. Google preview never falls back to the future community reference
renderer. This saved-preview boundary is separate from campaign creation and
does not make Performance Max buying generally available.
## OpenAI Ads preview boundary
The official OpenAI [Ads reference](https://developers.openai.com/ads/api-reference/ads)
names `POST /ads/{ad_id}/preview` and says the preview expires after 24 hours.
However, the official [OpenAPI schema](https://developers.openai.com/ads/openapi.json)
does not define that path, its response, an ad-preview URL field, or
preview-expiry metadata. The separate account `preview_url` is documented as a
[favicon preview](https://developers.openai.com/ads/api-reference/ad-account),
so Apostra does not use it as creative-preview output.
Apostra also does not approximate either OpenAI creative type. OpenAI does
not publish enough ChatGPT layout detail to reproduce a Chat Card faithfully,
and a Product Ad Template receives its image and destination from whichever
eligible feed item OpenAI selects when the ad serves, as described in the
[Product Feeds guide](https://developers.openai.com/ads/product-feeds). Until
OpenAI publishes a complete response contract and a selected-account proof
establishes a public HTTPS preview URL and exact expiry timestamp, OpenAI Ads
preview requests fail closed.
## Reference-renderer fallback
First-party platform previews remain authoritative. A future community
reference renderer may provide a clearly labeled approximation for a specific
canonical format family when a platform preview is unavailable. No adapter
currently advertises this fallback. It can become available only after that
exact family is proved faithful and its ephemeral preview store is merged,
deployed, configured, and able to enforce the full advertised link lifetime.
A code proposal or a permanent public creative bucket is not that boundary.
Until then, unsupported requests fail closed; an adapter with a first-party
implementation does not silently switch to a reference renderer. TikTok, Snap,
and Google have no such path today; their unsupported or unsynchronized inputs
fail closed. Shared creative-generation adapters follow the same boundary.
## Creative-production outputs are different
`preview_creative` shows how an ad platform renders a creative. A creative
production tool may instead return its finished reviewable asset directly from
`build_creative`. For example, [AudioStack](/v2/guides/ad-platform-compatibility/audiostack-creative)
returns the rendered `audio_hosted` asset, which can be played without a
separate preview call. That does not make AudioStack an ad-platform adapter or
advertise `preview_creative` for it.
Provider references: [Meta Business SDK preview surface](https://github.com/facebook/facebook-python-business-sdk/blob/main/facebook_business/adobjects/adcreative.py), [TikTok creative preview](https://business-api.tiktok.com/portal/docs?id=1739403070695426), [TikTok video search](https://business-api.tiktok.com/portal/docs?id=1740050472224769), [Snap creative preview](https://developers.snap.com/marketing-api/Ads-API/creatives), [Pinterest shareable previews](https://developers.pinterest.com/docs/work-with-ads/generate-shareable-ad-previews/), [Google ShareablePreviewService](https://developers.google.com/google-ads/api/reference/rpc/v25/ShareablePreviewService), and [OpenAI Ads preview reference](https://developers.openai.com/ads/api-reference/ads).
# Google Ads compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/google
What you can and cannot do with Google Ads through Apostra.
This page compares the Google Ads capabilities represented in the
Apostra adapter with what Apostra currently makes available to buyers.
It answers a practical question: **what can I do through Apostra today, and
what would still require another path?**
**Last reviewed 2026-08-28.** This is Apostra's description of
its own adapter, based on the implementation and the linked provider
documentation. Google Ads has not reviewed or endorsed this page.
Availability can still vary by account permissions, market, and provider access.
## At a glance
| Area | 🟢 Available | 🟡 Partial | 🔵 Testing | ⚪ Planned |
| - | - | - | - | - |
| Campaign products | **0** | **0** | **2** | **7** |
| Creative formats | **0** | **0** | **0** | **10** |
| Adapter capabilities | **1** | **0** | **0** | **15** |
### How to read availability
* 🟢 **Available** — Offered through Apostra now, subject to your ad account's permissions and eligibility with the provider.
* 🟡 **Partial** — Some of the workflow is available; the Comments column explains the boundary.
* 🔵 **Testing** — Apostra is validating the workflow before offering it broadly.
* ⚪ **Planned** — Not offered through Apostra yet. The provider may still support it directly; Comments explains why and links a roadmap reference.
## Roadmap
The adapter roadmap is tracked in AI-1951 (a plain-text reference — Apostra's internal issue tracker is not public).
This roadmap tracks Google Ads product completeness, staging readiness, and live readiness. Every row that is not currently available shows
that reference or a more specific one in its References column, so customers
and partners can quote a stable identifier when asking about a capability.
## Connection, accounts, and identity
Google Ads connects to Apostra through an OAuth grant. Connecting a Manager account (MCC) account gives Apostra access to each Customer account (10-digit ID) it can reach — you then map each one to your advertisers. The mapped account decides which Google Ads tenant a buy runs on: a media buy for an unmapped advertiser fails closed with `account_mapping_required` rather than silently using another account. Connection, account-discovery, and revocation behavior is the same across ad platforms — see [Platform accounts and advertiser mapping](/v2/concepts/ad-platform-accounts) for the full model and the [Google Ads section of the connecting guide](/v2/guides/connecting-ad-platforms#google-ads) for Google Ads-specific permissions.
| Apostra concept | Google Ads concept | What it represents |
| - | - | - |
| Account (`account`) | provider advertising account | The authenticated selected account is the execution boundary. |
| Media Buy (`media_buy`) | campaign | Media Buy maps to campaign. |
| Package (`package`) | ad group | Budget, targeting, optimization, and flight bindings. |
| Creative Assignment (`creative_assignment`) | ad or saved Performance Max asset group | Campaign creation does not return a Google-native preview. Separately, a synchronized Performance Max creative with exactly one saved assignment in the selected customer can return Google's expiring URL-only UI preview after exact campaign and asset-group readback. |
| Event Source (`event_source`) | conversion action on the effective conversion customer | The mapped advertiser customer is the campaign owner. Google may designate a different reachable customer as the conversion owner; the selected action must resolve there before a campaign is created. |
## Campaign products
| Feature | Availability | References | Comments |
| - | - | - | - |
| Search Traffic | 🔵 Testing | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Search Conversions | 🔵 Testing | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: both Apostra's implementation and the ad platform's API or account eligibility. |
| Search Ai Max Conversions | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
| Display Awareness | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
| Display Conversions | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
| Youtube Awareness | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Youtube Action | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Performance Max | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
| Demand Gen | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra has not completed the required catalog or activation-resource workflow for this product. Current boundary: Apostra implementation/testing. |
## Creative formats
These rows describe **Apostra availability**, not a claim that the ad platform
itself prohibits an unavailable format.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Responsive Search Ad | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Responsive Display Ad | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Video Ad Instream | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Video Ad Bumper | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Video Ad Non Skippable | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Performance Max | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Demand Gen | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Promoted Offerings | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Display | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Offerings | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
## What the adapter can do
| Feature | Availability | References | Comments |
| - | - | - | - |
| List Accounts | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Accounts | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Account synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Products | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Creatives | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| List Creatives | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Preview Creative | 🟢 Available | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) | Available only for an already synchronized Performance Max creative with one verified asset-group assignment in the selected customer. Apostra returns Google's expiring external preview link and does not invent placements, dimensions, thumbnails, embeds, or a transient fallback. |
| Create Media Buy | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Media Buys | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Update Media Buy | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Media Buy Delivery | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Signals | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Audiences | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Event Sources | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Log Event | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Catalogs | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Account Financials | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
## Placement choices
Apostra does not expose a separate Google Ads placement selector.
## Targeting
| Feature | Availability | References | Comments |
| - | - | - | - |
| Geo Countries | 🔵 Testing | [Google Ads targeting criterion fields](https://developers.google.com/google-ads/api/fields/v25/ad_group_criterion) · AI-1951 | Available on the two testing-stage Search products with presence-only delivery; selected-account eligibility still applies. Current boundary: Apostra implementation/testing. |
| Geo Regions | 🔵 Testing | [Google Ads targeting criterion fields](https://developers.google.com/google-ads/api/fields/v25/ad_group_criterion) · AI-1951 | Available on the two testing-stage Search products with presence-only delivery; selected-account eligibility still applies. Current boundary: Apostra implementation/testing. |
| Audience Include | 🔵 Testing | [Google Ads targeting criterion fields](https://developers.google.com/google-ads/api/fields/v25/ad_group_criterion) · AI-1951 | Available on the two testing-stage Search products for eligible governed Customer Match lists. Current boundary: Apostra implementation/testing. |
| Audience Exclude | 🔵 Testing | [Google Ads targeting criterion fields](https://developers.google.com/google-ads/api/fields/v25/ad_group_criterion) · AI-1951 | Available on the two testing-stage Search products for eligible governed Customer Match lists. Current boundary: Apostra implementation/testing. |
| Keyword Targets | 🔵 Testing | [Google Ads targeting criterion fields](https://developers.google.com/google-ads/api/fields/v25/ad_group_criterion) · AI-1951 | Available on the two testing-stage Search products with broad, phrase, and exact matching. Current boundary: Apostra implementation/testing. |
| Negative Keywords | 🔵 Testing | [Google Ads targeting criterion fields](https://developers.google.com/google-ads/api/fields/v25/ad_group_criterion) · AI-1951 | Available on the two testing-stage Search products with broad, phrase, and exact matching. Current boundary: Apostra implementation/testing. |
## Reporting
Apostra reports Google Ads delivery day by day (there is no hourly delivery breakdown), aggregated nightly from adapter-reported delivery, in the advertiser's home currency using the booked or historical exchange rate — any cross-currency rollup is always labeled an estimate and is never used for billing. See [Reporting overview](/v2/guides/reporting-overview) for the full model. Apostra does not currently publish an attribution-finality window for Google Ads; the table below lists every metric the adapter surfaces today.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Impressions | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Campaign and package totals |
| Spend | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Micros converted to account-currency major units |
| Clicks | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Campaign and package totals |
| Conversions | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Provider-attributed conversions |
| Conversion Value | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Provider-attributed conversion value |
| Breakdowns | ⚪ Planned | [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start) · AI-1951 | Country and device-type breakdowns |
## Webhooks and async lifecycle
Google Ads does not push status, delivery, or reporting events into Apostra — every campaign product declares `supports_webhooks: false`.
* **Status.** Apostra does not currently poll Google Ads in the background for creative or media-buy status changes; status reflects what the adapter returned on your last call.
* **Delivery.** Apostra has not yet made on-demand delivery fetching generally available for Google Ads — see Get Media Buy Delivery in "What the adapter can do" above. The `/reporting/metrics` endpoints never call the adapter either way — they read from a separately-populated store (see Reporting below), so a reporting request does not itself refresh provider delivery.
This is separate from Apostra's own buyer webhook subscriptions — an unrelated account-level event feed (currently `discovery.revision` only), documented in [Buyer webhooks](/v2/buyer/webhooks).
## Market and account availability
Apostra does not promise that every feature is enabled for every account or
market. Availability is checked against your selected ad account rather than a global market list on this page.
## Important limitations
* **Apostra implementation/testing.** Apostra reviewed 33 Google Ads creation flows. Search Traffic and a bounded Search Conversions workflow are implemented in the adapter; the remaining reviewed flows are blocked or unavailable.
* **Apostra implementation/testing.** Search Traffic and Search Conversions are still being tested before general availability. Search Conversions currently supports only website or phone-call actions in complete enabled-primary category/origin groups; other sources and arbitrary action subsets are not offered.
* **Apostra implementation/testing.** Product discovery and campaign budget writes use the selected advertiser customer's ISO 4217 currency. Foreign, manager, or invalid-currency customer snapshots; stale products priced in another currency; non-representable budget amounts; and inline or catalog-resolved offering allocations that do not reconcile exactly to their package budget fail before a Google write. Bid values use account-currency major units and may carry up to six decimal places because Google stores them as integer micros. On non-USD accounts, live forecasts keep the selected account currency while USD-authored advisory floors become zero instead of assuming an exchange rate.
* **both Apostra's implementation and the ad platform's API or account eligibility.** Search Conversions requires one event goal naming exact enabled primary website or phone-call conversion actions, explicit country or region targeting, and access to Google's effective conversion customer. Campaign creation stays paused while Apostra reads back the exact campaign goal and effective action set.
* **Apostra implementation/testing.** Creative assignment and publication are separate: campaign creation records creative IDs and applies canonical keyword targeting, while a later creative-sync call publishes the responsive search ad. Campaign creation does not return a Google-native creative preview.
* **both Apostra's implementation and the ad platform's API or account eligibility.** A separately synchronized Performance Max creative can return Google's expiring external UI preview only when one saved asset-group assignment and exact customer, campaign, asset-group, response, URL, and expiry readback agree. Google provides no placement selector or dimensions for this preview; ambiguous, transient, or fallback requests fail closed.
* **the ad platform's API or your account's eligibility.** Google Ads account permissions, conversion resources, audiences, and market eligibility can further narrow availability.
## Technical mapping
Engineering-level detail — raw provider objectives, billing enums, optimization
codes, and object-field mappings. Not required reading to understand what
Apostra can do with Google Ads; see the tables above for that.
| Product | Provider objective | Optimization | Billing |
| - | - | - | - |
| Search Traffic (`google_search_traffic`) | `not_exposed_in_public_config` | `TARGET_SPEND` | `not_exposed_in_public_config` |
| Search Conversions (`google_search_conversions`) | `not_exposed_in_public_config` | `MAXIMIZE_CONVERSIONS` | `not_exposed_in_public_config` |
| Search Ai Max Conversions (`google_search_ai_max_conversions`) | `not_exposed_in_public_config` | `MAXIMIZE_CONVERSIONS` | `not_exposed_in_public_config` |
| Display Awareness (`google_display_awareness`) | `not_exposed_in_public_config` | `TARGET_SPEND` | `not_exposed_in_public_config` |
| Display Conversions (`google_display_conversions`) | `not_exposed_in_public_config` | `MAXIMIZE_CONVERSIONS` | `not_exposed_in_public_config` |
| Youtube Awareness (`google_youtube_awareness`) | `not_exposed_in_public_config` | `TARGET_CPM` | `not_exposed_in_public_config` |
| Youtube Action (`google_youtube_action`) | `not_exposed_in_public_config` | `MAXIMIZE_CONVERSIONS` | `not_exposed_in_public_config` |
| Performance Max (`google_performance_max`) | `not_exposed_in_public_config` | `MAXIMIZE_CONVERSIONS` | `not_exposed_in_public_config` |
| Demand Gen (`google_demand_gen`) | `not_exposed_in_public_config` | `MAXIMIZE_CONVERSIONS` | `not_exposed_in_public_config` |
| Format | Google Ads shape |
| - | - |
| Responsive Search Ad (`google_responsive_search_ad`) | Canonical Code Format |
| Responsive Display Ad (`google_responsive_display_ad`) | Canonical Code Format |
| Video Ad Instream (`google_video_ad_instream`) | Canonical Code Format |
| Video Ad Bumper (`google_video_ad_bumper`) | Canonical Code Format |
| Video Ad Non Skippable (`google_video_ad_non_skippable`) | Canonical Code Format |
| Performance Max (`google_performance_max`) | Canonical Code Format |
| Demand Gen (`google_demand_gen`) | Canonical Code Format |
| Promoted Offerings (`google_promoted_offerings`) | Canonical Code Format |
| Generated Display (`google_generated_display`) | Canonical Code Format |
| Generated Offerings (`google_generated_offerings`) | Canonical Code Format |
| Targeting choice | Provider field |
| - | - |
| Geo Countries (`geo_countries`) | `The two testing-stage Search products resolve exact provider locations, require presence-only delivery, and read the selected account back before returning success.` |
| Geo Regions (`geo_regions`) | `The two testing-stage Search products resolve exact provider regions, require presence-only delivery, and read the selected account back before returning success.` |
| Audience Include (`audience_include`) | `Governed Customer Match inclusion uses targeting rather than observation mode and is read back on every target ad group.` |
| Audience Exclude (`audience_exclude`) | `Governed Customer Match suppression is resolved in the selected customer and read back on every target ad group.` |
| Keyword Targets (`keyword_targets`) | `Broad, phrase, and exact positive Search keywords are canonicalized, written at media-buy creation, and read back as an exact account-bound criterion set.` |
| Negative Keywords (`negative_keywords`) | `Broad, phrase, and exact negative Search keywords are canonicalized, written at media-buy creation, and read back as an exact account-bound criterion set.` |
| Apostra output | Google Ads source |
| - | - |
| Impressions (`impressions`) | `metrics.impressions` |
| Spend (`spend`) | `metrics.cost_micros` |
| Clicks (`clicks`) | `metrics.clicks` |
| Conversions (`conversions`) | `metrics.conversions` |
| Conversion Value (`conversion_value`) | `metrics.conversions_value` |
| Breakdowns (`breakdowns`) | `country \| device` |
## Provider documentation
* [Google Ads public documentation](https://developers.google.com/google-ads/api/docs/start)
* [Google Ads conversion goal documentation](https://developers.google.com/google-ads/api/docs/conversions/goals/campaign-goals)
* [Google Ads conversion customer reference](https://developers.google.com/google-ads/api/reference/rpc/v25/ConversionTrackingSetting)
* [Google Ads ShareablePreviewService reference](https://developers.google.com/google-ads/api/reference/rpc/v25/ShareablePreviewService)
* [Google Ads targeting criterion fields](https://developers.google.com/google-ads/api/fields/v25/ad_group_criterion)
* [Google Ads audience targeting settings](https://developers.google.com/google-ads/api/docs/targeting/targeting-settings)
# Ad platform compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/index
What each connected ad platform can do through Apostra.
Use these pages to compare what each ad platform offers through Apostra.
Every page uses the same four columns — Feature, Availability, References, and
Comments — so you can scan what is available now, what is partial or still
being tested, and what Apostra does not currently offer, without needing
to know any internal engineering terms.
The catalog was last updated **2026-08-29**. Account permissions,
markets, and platform access can narrow what is available for a specific account.
Object mappings and reporting describe documented integration behavior; they
are not exhaustive inventories of every provider field or report.
See available products, creative formats, targeting, reporting, and current gaps.
See available products, creative formats, targeting, reporting, and current gaps.
See available products, creative formats, targeting, reporting, and current gaps.
See available products, creative formats, targeting, reporting, and current gaps.
See available products, creative formats, targeting, reporting, and current gaps.
See available products, creative formats, targeting, reporting, and current gaps.
See available products, creative formats, targeting, reporting, and current gaps.
See available products, creative formats, targeting, reporting, and current gaps.
See available products, creative formats, targeting, reporting, and current gaps.
See available products, creative formats, targeting, reporting, and current gaps.
AudioStack is a separate creative-only integration and does not provide media
buying through Apostra. See
[AudioStack creative-tool compatibility](/v2/guides/ad-platform-compatibility/audiostack-creative).
## What the labels mean
| Label | Meaning |
| - | - |
| 🟢 **Available** | Offered through Apostra now, subject to your ad account's permissions and eligibility with the provider. |
| 🟡 **Partial** | Some of the workflow is available; the Comments column explains the boundary. |
| 🔵 **Testing** | Apostra is validating the workflow before offering it broadly. |
| ⚪ **Planned** | Not offered through Apostra yet. The provider may still support it directly; Comments explains why and links a roadmap reference. |
Each provider page's Comments column names the boundary — Apostra
implementation/testing, the ad platform's API or account eligibility, or both —
so an Apostra product gap is never mistaken for a platform prohibition. It
also shows the roadmap reference as plain text for every incomplete
capability, giving customers and partners a stable identifier without
publishing a private issue-tracker link or implying a delivery date. Raw
provider codes and field mappings live in each page's Technical mapping
section; provider documentation is linked in References and at the bottom of
each page for readers who need the native API details.
# LinkedIn Ads compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/linkedin
What you can and cannot do with LinkedIn Ads through Apostra.
This page compares the LinkedIn Ads capabilities represented in the
Apostra adapter with what Apostra currently makes available to buyers.
It answers a practical question: **what can I do through Apostra today, and
what would still require another path?**
**Last reviewed 2026-08-18.** This is Apostra's description of
its own adapter, based on the implementation and the linked provider
documentation. LinkedIn Ads has not reviewed or endorsed this page.
Availability can still vary by account permissions, market, and provider access.
## At a glance
| Area | 🟢 Available | 🟡 Partial | 🔵 Testing | ⚪ Planned |
| - | - | - | - | - |
| Campaign products | **0** | **0** | **4** | **6** |
| Creative formats | **0** | **0** | **1** | **11** |
| Adapter capabilities | **2** | **0** | **0** | **14** |
### How to read availability
* 🟢 **Available** — Offered through Apostra now, subject to your ad account's permissions and eligibility with the provider.
* 🟡 **Partial** — Some of the workflow is available; the Comments column explains the boundary.
* 🔵 **Testing** — Apostra is validating the workflow before offering it broadly.
* ⚪ **Planned** — Not offered through Apostra yet. The provider may still support it directly; Comments explains why and links a roadmap reference.
## Roadmap
The adapter roadmap is tracked in AI-3806 (a plain-text reference — Apostra's internal issue tracker is not public).
This roadmap tracks LinkedIn product completeness, provider access, and live readiness. Every row that is not currently available shows
that reference or a more specific one in its References column, so customers
and partners can quote a stable identifier when asking about a capability.
## Connection, accounts, and identity
LinkedIn Ads connects to Apostra through an OAuth grant. Connecting a Organization account gives Apostra access to each Ad account (sponsored account) it can reach — you then map each one to your advertisers. The mapped account decides which LinkedIn Ads tenant a buy runs on: a media buy for an unmapped advertiser fails closed with `account_mapping_required` rather than silently using another account. Connection, account-discovery, and revocation behavior is the same across ad platforms — see [Platform accounts and advertiser mapping](/v2/concepts/ad-platform-accounts) for the full model and the [LinkedIn Ads section of the connecting guide](/v2/guides/connecting-ad-platforms#linkedin) for LinkedIn Ads-specific permissions.
| Apostra concept | LinkedIn Ads concept | What it represents |
| - | - | - |
| Account (`account`) | provider advertising account | The authenticated selected account is the execution boundary. |
| Media Buy (`media_buy`) | campaign | One exact campaign remains the media-buy identity and validates its account-owned parent campaign group. New campaigns commit independently versioned durable package or catalog inventory before activation and fail closed if it is lost; legacy unmarked campaigns are labeled unverified and never claim catalog attribution. |
| Package (`package`) | campaign | Budget, targeting, optimization, and flight bindings. |
| Creative Assignment (`creative_assignment`) | creative | Creative materialization or assignment surface declared by current code. |
## Campaign products
| Feature | Availability | References | Comments |
| - | - | - | - |
| Sponsored Content | 🔵 Testing | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Brand Awareness Reach | 🔵 Testing | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Video Ads | 🔵 Testing | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Lead Gen | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra has implemented account-owned Company Page and published Lead Gen Form discovery, exact binding, paused creation, and readback in a restricted test path. General availability still requires end-to-end validation. Current boundary: Apostra implementation/testing. |
| Website Traffic | 🔵 Testing | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Message Ads | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Conversions | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra has implemented account-owned conversion-rule discovery, exact campaign-conversion association, paused creation, and readback in a restricted test path. General availability still requires end-to-end validation. Current boundary: Apostra implementation/testing. |
| Text Ads | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Follower Ads | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Job Ads | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
## Creative formats
These rows describe **Apostra availability**, not a claim that the ad platform
itself prohibits an unavailable format.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Single Image | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Video | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Carousel | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Lead Gen | 🔵 Testing | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra is still testing this creative format before making it generally available. Current boundary: Apostra implementation/testing. |
| Message Ad | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Text Ad | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Spotlight | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Follower | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Promoted Offerings | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Image | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Video | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Offerings | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
## What the adapter can do
| Feature | Availability | References | Comments |
| - | - | - | - |
| List Accounts | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Accounts | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Account synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Products | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra returns four campaign products and reviewable brief decisions bound to the selected account and rejects changed assisted-execution inputs. Lead Gen, Website Conversions, and professional targeting are still restricted while end-to-end validation is completed. Current boundary: Apostra implementation/testing. |
| Sync Creatives | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| List Creatives | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Preview Creative | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | LinkedIn supports saved and transient previews, but Apostra must first verify the selected account and saved creative, extract only a safe preview link, and preserve the documented three-hour maximum lifetime. Transient inline HTML remains unavailable. Current boundary: Apostra implementation/testing. |
| Create Media Buy | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Media Buys | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Update Media Buy | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Media Buy Delivery | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra validates account ownership, campaign identity, account currency, and package-level metrics, but on-demand delivery remains in testing before general availability. Current boundary: Apostra implementation/testing. |
| Get Signals | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Audiences | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Event Sources | 🟢 Available | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) | If your LinkedIn connection predates this release, reconnect it to grant rw\_conversions; then call sync\_event\_sources again and replace any stored event\_source\_id — the previous synthetic identifier no longer resolves. Returns one event source per enabled LinkedIn Conversions API rule in the selected account instead of a single synthetic tag derived from the account ID. An account with more than 500 eligible rules gets a clear error on unfiltered discovery instead of a partial list — request the specific event\_source\_id values you need instead. Those account-scoped exact reads are chunked to 100 IDs, do not drain the full inventory, and treat only an explicit LinkedIn 404 as absent; permission, throttling, malformed-response, and provider failures remain visible errors. |
| Log Event | 🟢 Available | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) | Accepts events only for a conversion rule discovered by sync\_event\_sources that is still enabled and owned by the selected account, and rejects an event whose type doesn't match that rule. Every event must identify the user with a lowercase SHA-256 hashed email, a LinkedIn li\_fat\_id, or one advertiser-owned external ID. If custom\_data.value is present, custom\_data.currency is required; Apostra never assumes USD. Invalid input rejects the complete call before anything is sent to LinkedIn. |
| Sync Catalogs | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Account Financials | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
## Placement choices
| Feature | Availability | References | Comments |
| - | - | - | - |
| LinkedIn Feed | 🟡 Partial | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra's current sponsored-content products use the LinkedIn Feed and do not expose a separate placement selector. Current boundary: Apostra implementation/testing. |
## Targeting
| Feature | Availability | References | Comments |
| - | - | - | - |
| Geo Countries | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Geo Regions | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Signal Targeting Groups | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Signals | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Audience Include | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Audience Exclude | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
## Reporting
Apostra reports LinkedIn Ads delivery day by day (there is no hourly delivery breakdown), aggregated nightly from adapter-reported delivery, in the advertiser's home currency using the booked or historical exchange rate — any cross-currency rollup is always labeled an estimate and is never used for billing. See [Reporting overview](/v2/guides/reporting-overview) for the full model. Apostra does not currently publish an attribution-finality window for LinkedIn Ads; the table below lists every metric the adapter surfaces today.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Impressions | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Campaign and package totals |
| Spend | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Selected-account currency |
| Clicks | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Campaign and package totals |
| Completed Views | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Completed video plays |
| Conversions | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Website conversions |
| Leads | ⚪ Planned | [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/) · AI-3806 | Native lead submissions |
## Webhooks and async lifecycle
LinkedIn Ads does not push status, delivery, or reporting events into Apostra — every campaign product declares `supports_webhooks: false`.
* **Status.** Apostra does not currently poll LinkedIn Ads in the background for creative or media-buy status changes; status reflects what the adapter returned on your last call.
* **Delivery.** Apostra has not yet made on-demand delivery fetching generally available for LinkedIn Ads — see Get Media Buy Delivery in "What the adapter can do" above. The `/reporting/metrics` endpoints never call the adapter either way — they read from a separately-populated store (see Reporting below), so a reporting request does not itself refresh provider delivery.
This is separate from Apostra's own buyer webhook subscriptions — an unrelated account-level event feed (currently `discovery.revision` only), documented in [Buyer webhooks](/v2/buyer/webhooks).
## Market and account availability
Apostra does not promise that every feature is enabled for every account or
market. Availability is checked against your selected ad account rather than a global market list on this page.
## Important limitations
* **Apostra implementation/testing.** Apostra has reviewed 18 LinkedIn creation flows, but this is not yet a confirmed complete catalog of every LinkedIn Ads workflow.
* **Apostra implementation/testing.** Four campaign products exist in the current adapter code but are still being certified before general availability.
* **Apostra implementation/testing.** Lead Gen and Website Conversions are still being tested and are not generally available; the other unavailable product rows have not been implemented end to end.
* **the ad platform's API or your account's eligibility.** LinkedIn account permissions, forms, audiences, and objective eligibility can further narrow availability.
## Technical mapping
Engineering-level detail — raw provider objectives, billing enums, optimization
codes, and object-field mappings. Not required reading to understand what
Apostra can do with LinkedIn Ads; see the tables above for that.
| Product | Provider objective | Optimization | Billing |
| - | - | - | - |
| Sponsored Content (`linkedin_sponsored_content`) | `BRAND_AWARENESS` | `MAX_IMPRESSION` | `CPM` |
| Brand Awareness Reach (`linkedin_brand_awareness_reach`) | `BRAND_AWARENESS` | `MAX_REACH` | `CPM` |
| Video Ads (`linkedin_video_ads`) | `VIDEO_VIEW` | `MAX_VIDEO_VIEW` | `CPM` |
| Lead Gen (`linkedin_lead_gen`) | `LEAD_GENERATION` | `MAX_LEAD` | `CPM` |
| Website Traffic (`linkedin_website_traffic`) | `WEBSITE_VISIT` | `MAX_CLICK` | `CPC` |
| Message Ads (`linkedin_message_ads`) | `ENGAGEMENT` | `MAX_CLICK` | `CPC` |
| Conversions (`linkedin_conversions`) | `WEBSITE_CONVERSION` | `MAX_CONVERSION` | `CPM` |
| Text Ads (`linkedin_text_ads`) | `WEBSITE_VISIT` | `NONE` | `CPC` |
| Follower Ads (`linkedin_follower_ads`) | `ENGAGEMENT` | `MAX_IMPRESSION` | `CPM` |
| Job Ads (`linkedin_job_ads`) | `JOB_APPLICANT` | `MAX_IMPRESSION` | `CPM` |
| Format | LinkedIn Ads shape |
| - | - |
| Single Image (`linkedin_single_image`) | Canonical Code Format |
| Video (`linkedin_video`) | Canonical Code Format |
| Carousel (`linkedin_carousel`) | Canonical Code Format |
| Lead Gen (`linkedin_lead_gen`) | Canonical Code Format |
| Message Ad (`linkedin_message_ad`) | Canonical Code Format |
| Text Ad (`linkedin_text_ad`) | Canonical Code Format |
| Spotlight (`linkedin_spotlight`) | Canonical Code Format |
| Follower (`linkedin_follower`) | Canonical Code Format |
| Promoted Offerings (`linkedin_promoted_offerings`) | Canonical Code Format |
| Generated Image (`linkedin_generated_image`) | Canonical Code Format |
| Generated Video (`linkedin_generated_video`) | Canonical Code Format |
| Generated Offerings (`linkedin_generated_offerings`) | Canonical Code Format |
| Targeting choice | Provider field |
| - | - |
| Geo Countries (`geo_countries`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Geo Regions (`geo_regions`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Signal Targeting Groups (`signal_targeting_groups`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Signals (`signals`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Audience Include (`audience_include`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Audience Exclude (`audience_exclude`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Apostra output | LinkedIn Ads source |
| - | - |
| Impressions (`impressions`) | `impressions` |
| Spend (`spend`) | `costInLocalCurrency` |
| Clicks (`clicks`) | `clicks` |
| Completed Views (`completed_views`) | `videoCompletions` |
| Conversions (`conversions`) | `externalWebsiteConversions` |
| Leads (`leads`) | `oneClickLeads` |
## Provider documentation
* [LinkedIn Ads public documentation](https://learn.microsoft.com/en-us/linkedin/marketing/)
* [LinkedIn preview reference](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/ad-preview?view=li-lms-2026-07)
# Meta Ads compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/meta
What you can and cannot do with Meta Ads through Apostra.
This page compares the Meta Ads capabilities represented in the
Apostra adapter with what Apostra currently makes available to buyers.
It answers a practical question: **what can I do through Apostra today, and
what would still require another path?**
**Last reviewed 2026-08-27.** This is Apostra's description of
its own adapter, based on the implementation and the linked provider
documentation. Meta Ads has not reviewed or endorsed this page.
Availability can still vary by account permissions, market, and provider access.
## At a glance
| Area | 🟢 Available | 🟡 Partial | 🔵 Testing | ⚪ Planned |
| - | - | - | - | - |
| Campaign products | **10** | **0** | **16** | **1** |
| Creative formats | **1** | **0** | **4** | **7** |
| Adapter capabilities | **15** | **0** | **0** | **1** |
### How to read availability
* 🟢 **Available** — Offered through Apostra now, subject to your ad account's permissions and eligibility with the provider.
* 🟡 **Partial** — Some of the workflow is available; the Comments column explains the boundary.
* 🔵 **Testing** — Apostra is validating the workflow before offering it broadly.
* ⚪ **Planned** — Not offered through Apostra yet. The provider may still support it directly; Comments explains why and links a roadmap reference.
## Roadmap
The adapter roadmap is tracked in AI-1940 (a plain-text reference — Apostra's internal issue tracker is not public).
This roadmap tracks Meta adapter product completeness, safety, and live readiness. Every row that is not currently available shows
that reference or a more specific one in its References column, so customers
and partners can quote a stable identifier when asking about a capability.
## Connection, accounts, and identity
Meta Ads connects to Apostra through an OAuth grant. Connecting a Business Manager / Business Portfolio account gives Apostra access to each Ad account (`act_…`) it can reach — you then map each one to your advertisers. The mapped account decides which Meta Ads tenant a buy runs on: a media buy for an unmapped advertiser fails closed with `account_mapping_required` rather than silently using another account. Connection, account-discovery, and revocation behavior is the same across ad platforms — see [Platform accounts and advertiser mapping](/v2/concepts/ad-platform-accounts) for the full model and the [Meta Ads section of the connecting guide](/v2/guides/connecting-ad-platforms#meta) for Meta Ads-specific permissions.
| Apostra concept | Meta Ads concept | What it represents |
| - | - | - |
| Account (`account`) | ad account | Explicit selected ad account under the authorized Meta business. |
| Media Buy (`media_buy`) | campaign | Campaign objective and lifecycle. |
| Package (`package`) | ad set | Optimization, billing event, budget, flight, targeting, placement, and conversion source. |
| Creative (`creative`) | ad creative | Image, video, existing-post, and governed creative shapes. |
| Creative Assignment (`creative_assignment`) | ad | Ad binds one creative to one ad set. |
| Signal (`signal`) | Custom or Lookalike Audience | Account-owned audience identity and readiness. |
## Campaign products
| Feature | Availability | References | Comments |
| - | - | - | - |
| Awareness | 🟢 Available | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) | Available through Apostra. |
| Awareness Impressions | 🟢 Available | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) | Available through Apostra. |
| Awareness Ad Recall | 🟢 Available | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) | Available through Apostra. |
| Traffic | 🟢 Available | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) | Available through Apostra. |
| Traffic Landing Page Views | 🟢 Available | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) | Available through Apostra. |
| Engagement Page Likes | ⚪ Planned | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Meta no longer accepts PAGE\_LIKES as an optimization goal for OUTCOME\_ENGAGEMENT campaigns. Use meta\_engagement for general Page engagement instead. Current boundary: Apostra implementation/testing. |
| Engagement Event Responses | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| Engagement Instagram Profile Visits | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| Engagement Instagram Conversations | 🟢 Available | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) | Available through Apostra. |
| Engagement | 🟢 Available | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) | Available through Apostra. |
| Engagement Video Views | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Not available through standard buyer discovery or execution. This ThruPlay product is retained only for the protected Meta video conformance path because all of its creative formats are Testing. Current boundary: Apostra implementation/testing. |
| Engagement Messenger | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| Engagement Whatsapp | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| Conversions | 🟢 Available | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) | Available through Apostra. |
| Sales Website Value | 🟢 Available | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) | Available through Apostra. |
| Sales Website Catalog | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| Sales Shop | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| App Installs | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| App Post Install Event | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| App Conversion Value | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| Leads | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| Leads Instant Form Quality | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| Leads Website | 🟢 Available | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) | Available through Apostra. |
| Leads Messenger | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| Leads Instagram | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| Leads Whatsapp | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
| Leads Calls | 🔵 Testing | [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group) · AI-1940 | Apostra is verifying this product end to end before offering it through standard buyer discovery and media buy creation. Current boundary: Apostra implementation/testing. |
## Pricing notes
`pricing_options` omits `floor_price` and `price_guidance` for every Meta product. Meta exposes no discovery-time bid estimate, so a static guess would be no more reliable than no guidance at all, and for a non-USD ad account it would mean silently relabeling a USD number or, as it did before, showing an outright zero. Each product is still priced (`pricing_model`, `currency`, `max_bid`); there is simply no platform-backed number to bid against yet.
The Sales Website Value product (`meta_sales_website_value`) never accepts a `bidPrice`: Meta only runs it as automatic delivery driven by an optimization goal. When adding this product, omit `bidPrice` and instead declare one of two optimization-goal shapes — `target: { kind: "maximize_value" }`, or a ROAS floor (`target: { kind: "per_ad_spend", strength: "floor" }`) — either directly on the product, or once for the whole campaign via the campaign's `performanceConfig.optimizationGoals` (see [Optimization goal target strength](/v2/object-guides/campaign#optimization-goal-target-strength-performanceconfig)). A ROAS floor's `strength` is typed only at the campaign level today, not on the media-buy/product-level goal shape, so declare it there when this product needs one. Supplying a `bidPrice` for this product is rejected; supplying neither a bid nor a satisfying goal at either level is also rejected, both at `add_discovery_products` time and again at execution.
Every other Meta product accepts an omitted `bidPrice` — no optimization-goal shape required. Awareness, Traffic, Engagement, Leads, App Promotion (App Install Campaign, Post-install Event), and plain Conversions/Sales products all deliver at Meta's own automatic, highest-volume no-cap bid strategy when `bidPrice` is left out, the same delivery mode Ads Manager offers as "Highest volume" for these objectives. Supplying a `bidPrice` still works exactly as before and sets Meta's bid-cap ceiling. The one remaining exception is App Promotion — Maximize In-app Value (`meta_app_conversion_value`): it still requires a `bidPrice` today even though Meta does not use it for this product either; this is a known gap, not an intentional value-optimization contract like Sales Website Value's, and is tracked for a future fix.
## Creative formats
These rows describe **Apostra availability**, not a claim that the ad platform
itself prohibits an unavailable format.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Image Feed | 🟢 Available | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) | Available through Apostra. |
| Video Feed | 🔵 Testing | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) · AI-1940 | Not available through standard buyer sync or assignment. The current Testing path requires an explicit JPEG or PNG \`thumbnail\` matching the video. Automatic thumbnail selection is not available through standard buyer workflows. Current boundary: Apostra implementation/testing. |
| Existing Post | 🔵 Testing | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) · AI-1940 | Apostra is still testing this creative format before making it generally available. Current boundary: Apostra implementation/testing. |
| Stories | 🔵 Testing | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) · AI-1940 | Not available through standard buyer sync or assignment. The current Testing path requires an explicit JPEG or PNG \`thumbnail\` matching the video. Automatic thumbnail selection is not available through standard buyer workflows. Current boundary: Apostra implementation/testing. |
| Reels | 🔵 Testing | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) · AI-1940 | Not available through standard buyer sync or assignment. The current Testing path requires an explicit JPEG or PNG \`thumbnail\` matching the video. Automatic thumbnail selection is not available through standard buyer workflows. Current boundary: Apostra implementation/testing. |
| Carousel | ⚪ Planned | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) · AI-1940 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Promoted Offerings | ⚪ Planned | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) · AI-1940 | Promoted Offerings is built through the per-offering package flow on \`create\_media\_buy\`. No Meta product currently carries it as a selectable creative format, so it is not offered in the creative catalog. Current boundary: Apostra implementation/testing. |
| Generated Image 1x1 | ⚪ Planned | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) · AI-1940 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Stories 9x16 | ⚪ Planned | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) · AI-1940 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Video Feed | ⚪ Planned | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) · AI-1940 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Reels 9x16 | ⚪ Planned | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) · AI-1940 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Offerings | ⚪ Planned | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) · AI-1940 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
## What the adapter can do
| Feature | Availability | References | Comments |
| - | - | - | - |
| List Accounts | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Business/ad-account discovery and explicit selection |
| Sync Accounts | ⚪ Planned | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) · AI-1940 | Account synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Products | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | 21 objective/optimization/billing mappings |
| Sync Creatives | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Creative creation and assignment readback for formats marked Available above; Testing formats are not available through standard buyer workflows. |
| List Creatives | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Ad creative inventory and assignment projection |
| Preview Creative | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Provider-rendered, URL-only previews for verified selected-account creatives and transient image manifests |
| Create Media Buy | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Campaign → ad set → ad |
| Get Media Buys | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Campaign/ad-set/ad projection with targeting provenance |
| Update Media Buy | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Lifecycle, geography, audiences, placements, devices, and creative assignments; AdCP 3.2 budget/bid replacement is not atomic |
| Get Media Buy Delivery | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Insights API campaign/ad-set reporting plus placement, device, and geography breakdowns |
| Get Signals | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Custom and Lookalike Audience discovery |
| Sync Audiences | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Custom Audience create/replace/status lifecycle |
| Sync Event Sources | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Pixel, app, and supported built-in event source discovery |
| Log Event | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Conversions API event ingestion. Meta records an offline event as other. Meta refuses offline and other events more than seven days old, so Apostra rejects each such event on its own and sends the rest of the batch. |
| Sync Catalogs | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Catalog synchronization |
| Get Account Financials | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Account currency, funding, and spend constraints |
## Placement choices
| Feature | Availability | References | Comments |
| - | - | - | - |
| Facebook Feed | 🟢 Available | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) | facebook / feed |
| Facebook Stories | 🟢 Available | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) | facebook / story |
| Facebook Reels | 🟢 Available | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) | Available through Apostra. |
| Instagram Feed | 🟢 Available | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) | instagram / stream |
| Instagram Stories | 🟢 Available | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) | instagram / story |
| Instagram Reels | 🟢 Available | [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign) | instagram / reels |
## Targeting
| Feature | Availability | References | Comments |
| - | - | - | - |
| Geo Countries | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Available through Apostra. |
| Geo Regions | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | ISO subdivision → provider region key with provenance |
| Geo Places | 🟡 Partial | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) · AI-1940 | Negotiated AdCP 3.2 stable provider city IDs; seller-wide advertisement awaits the authenticated resolver endpoint. Current boundary: Apostra implementation/testing. |
| Geo Postal Areas | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Available through Apostra. |
| Ext | 🟡 Partial | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) · AI-1940 | Targeting choice is only partially available through Apostra. Current boundary: Apostra implementation/testing. |
| Age Ranges | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Available through Apostra. |
| User Age Unknown | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Explicit unknown-age policy |
| Genders | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Available through Apostra. |
| Device Type | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Available through Apostra. |
| Audience Include | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Available through Apostra. |
| Audience Exclude | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Available through Apostra. |
| Signal Targeting Groups | 🟡 Partial | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) · AI-1940 | Create one outer all expression with binary-true Facebook signals, at most one any group, and any number of none exclusions. Other expressions return UNSUPPORTED\_TARGETING\_DIMENSION before Meta is called. Signal-targeting updates will be available in a later release. Current boundary: Apostra implementation/testing. |
| Frequency Cap | 🟢 Available | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) | Available through Apostra. |
| Placement Selection | 🟡 Partial | [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview) · AI-1940 | Selecting Facebook Feed and/or Instagram Feed together with Stories and/or Reels is fully honored; selecting only Stories and/or Reels without either Feed is not yet available on every account. Current boundary: Apostra implementation/testing. |
## Reporting
Apostra reports Meta Ads delivery day by day (there is no hourly delivery breakdown), aggregated nightly from adapter-reported delivery, in the advertiser's home currency using the booked or historical exchange rate — any cross-currency rollup is always labeled an estimate and is never used for billing. See [Reporting overview](/v2/guides/reporting-overview) for the full model. Apostra does not currently publish an attribution-finality window for Meta Ads; the table below lists every metric the adapter surfaces today.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Impressions | 🟢 Available | [Meta Insights API](https://developers.facebook.com/docs/marketing-api/insights) | Campaign and ad-set totals |
| Clicks | 🟢 Available | [Meta Insights API](https://developers.facebook.com/docs/marketing-api/insights) | Outbound navigation only; broad Meta clicks are excluded |
| Spend | 🟢 Available | [Meta Insights API](https://developers.facebook.com/docs/marketing-api/insights) | Account-currency amount |
| Reach | 🟢 Available | [Meta Insights API](https://developers.facebook.com/docs/marketing-api/insights) | Unique accounts when returned |
| Conversions | 🟢 Available | [Meta Insights API](https://developers.facebook.com/docs/marketing-api/insights) | Mapped according to the committed optimization contract |
| Completed Views | 🟢 Available | [Meta Insights API](https://developers.facebook.com/docs/marketing-api/insights) | ThrUPlay mapping |
| Breakdowns | 🟢 Available | [Meta Insights API](https://developers.facebook.com/docs/marketing-api/insights) | Placement, device, and geography projections with reconciliation |
## Webhooks and async lifecycle
Meta Ads campaign products declare `supports_webhooks: false`, so there is no product-level provider webhook contract for status, delivery, or reporting. Apostra separately accepts Meta's native ad-account webhook events.
* **Status.** A native Meta ad-account webhook event can trigger a media-buy status refresh for affected campaigns. Apostra does not otherwise poll Meta Ads in the background for creative or media-buy status changes; an explicit adapter read refreshes provider campaign state.
* **Delivery.** Apostra fetches delivery numbers (spend, impressions, clicks) live from Meta Ads on demand, only when you call `get_media_buy_delivery` directly. The `/reporting/metrics` endpoints never call the adapter either way — they read from a separately-populated store (see Reporting below), so a reporting request does not itself refresh provider delivery.
This is separate from Apostra's own buyer webhook subscriptions — an unrelated account-level event feed (currently `discovery.revision` only), documented in [Buyer webhooks](/v2/buyer/webhooks).
Meta is the one adapter with a genuine provider-pushed channel: Apostra can receive Meta's native ad-account webhook events and surface account-health signals such as disapproved ads or creative fatigue as `ext.scope3_provider_signals`, a Scope3 extension rather than a canonical AdCP status field. Processing an event can also refresh media-buy status for affected campaigns. The channel does not carry delivery numbers; on-demand delivery reads and the separately populated reporting store remain the sources for delivery.
## Market and account availability
Apostra does not promise that every feature is enabled for every account or
market. Availability is checked against your selected ad account rather than a global market list on this page.
## Important limitations
* **Apostra implementation/testing.** Messenger Inbox, Messenger Stories, and Threads Feed are not offered as selectable placements.
* **Apostra implementation/testing.** Promoting an existing post is still being tested and is not generally available.
* **Apostra implementation/testing.** Budget and bidding settings cannot be replaced atomically; create a replacement media buy instead.
* **the ad platform's API or your account's eligibility.** Account permissions, Page, Instagram, and WhatsApp ownership, and objective eligibility can narrow what is available.
* **the ad platform's API or your account's eligibility.** Sales Website Value (\`meta\_sales\_website\_value\`) requires the ad account's business to have been actively advertising for several weeks or to be verified with Meta; new or unverified businesses are rejected until then. Use Conversions (\`meta\_conversions\`) instead in the meantime.
* **the ad platform's API.** The Awareness product (\`meta\_awareness\`) uses Meta's REACH optimization goal. When no \`targeting\_overlay.frequency\_cap\` is provided, Meta automatically applies a platform default of 2 impressions per 7 days. To use a different cap, pass \`targeting\_overlay.frequency\_cap\` explicitly in your \`save\_media\_buy\` or \`create\_media\_buys\` call. All other Meta products use auction delivery and have no platform-applied default frequency cap.
## Technical mapping
Engineering-level detail — raw provider objectives, billing enums, optimization
codes, and object-field mappings. Not required reading to understand what
Apostra can do with Meta Ads; see the tables above for that.
| Product | Provider objective | Optimization | Billing |
| - | - | - | - |
| Awareness (`meta_awareness`) | `OUTCOME_AWARENESS` | `REACH` | `IMPRESSIONS` |
| Awareness Impressions (`meta_awareness_impressions`) | `OUTCOME_AWARENESS` | `IMPRESSIONS` | `IMPRESSIONS` |
| Awareness Ad Recall (`meta_awareness_ad_recall`) | `OUTCOME_AWARENESS` | `AD_RECALL_LIFT` | `IMPRESSIONS` |
| Traffic (`meta_traffic`) | `OUTCOME_TRAFFIC` | `LINK_CLICKS` | `IMPRESSIONS` |
| Traffic Landing Page Views (`meta_traffic_landing_page_views`) | `OUTCOME_TRAFFIC` | `LANDING_PAGE_VIEWS` | `IMPRESSIONS` |
| Engagement Page Likes (`meta_engagement_page_likes`) | `OUTCOME_ENGAGEMENT` | `PAGE_LIKES` | `PAGE_LIKES` |
| Engagement Event Responses (`meta_engagement_event_responses`) | `OUTCOME_ENGAGEMENT` | `EVENT_RESPONSES` | `IMPRESSIONS` |
| Engagement Instagram Profile Visits (`meta_engagement_instagram_profile_visits`) | `OUTCOME_ENGAGEMENT` | `PROFILE_VISIT` | `IMPRESSIONS` |
| Engagement Instagram Conversations (`meta_engagement_instagram_conversations`) | `OUTCOME_ENGAGEMENT` | `CONVERSATIONS` | `IMPRESSIONS` |
| Engagement (`meta_engagement`) | `OUTCOME_ENGAGEMENT` | `POST_ENGAGEMENT` | `IMPRESSIONS` |
| Engagement Video Views (`meta_engagement_video_views`) | `OUTCOME_ENGAGEMENT` | `THRUPLAY` | `IMPRESSIONS` |
| Engagement Messenger (`meta_engagement_messenger`) | `OUTCOME_ENGAGEMENT` | `CONVERSATIONS` | `IMPRESSIONS` |
| Engagement Whatsapp (`meta_engagement_whatsapp`) | `OUTCOME_ENGAGEMENT` | `CONVERSATIONS` | `IMPRESSIONS` |
| Conversions (`meta_conversions`) | `OUTCOME_SALES` | `OFFSITE_CONVERSIONS` | `IMPRESSIONS` |
| Sales Website Value (`meta_sales_website_value`) | `OUTCOME_SALES` | `VALUE` | `IMPRESSIONS` |
| Sales Website Catalog (`meta_sales_website_catalog`) | `OUTCOME_SALES` | `OFFSITE_CONVERSIONS` | `IMPRESSIONS` |
| Sales Shop (`meta_sales_shop`) | `OUTCOME_SALES` | `OFFSITE_CONVERSIONS` | `IMPRESSIONS` |
| App Installs (`meta_app_installs`) | `OUTCOME_APP_PROMOTION` | `APP_INSTALLS` | `IMPRESSIONS` |
| App Post Install Event (`meta_app_post_install_event`) | `OUTCOME_APP_PROMOTION` | `APP_INSTALLS_AND_OFFSITE_CONVERSIONS` | `IMPRESSIONS` |
| App Conversion Value (`meta_app_conversion_value`) | `OUTCOME_APP_PROMOTION` | `IN_APP_VALUE` | `IMPRESSIONS` |
| Leads (`meta_leads`) | `OUTCOME_LEADS` | `LEAD_GENERATION` | `IMPRESSIONS` |
| Leads Instant Form Quality (`meta_leads_instant_form_quality`) | `OUTCOME_LEADS` | `QUALITY_LEAD` | `IMPRESSIONS` |
| Leads Website (`meta_leads_website`) | `OUTCOME_LEADS` | `OFFSITE_CONVERSIONS` | `IMPRESSIONS` |
| Leads Messenger (`meta_leads_messenger`) | `OUTCOME_LEADS` | `LEAD_GENERATION` | `IMPRESSIONS` |
| Leads Instagram (`meta_leads_instagram`) | `OUTCOME_LEADS` | `LEAD_GENERATION` | `IMPRESSIONS` |
| Leads Whatsapp (`meta_leads_whatsapp`) | `OUTCOME_LEADS` | `LEAD_GENERATION` | `IMPRESSIONS` |
| Leads Calls (`meta_leads_calls`) | `OUTCOME_LEADS` | `QUALITY_CALL` | `IMPRESSIONS` |
| Format | Meta Ads shape |
| - | - |
| Image Feed (`meta_image_feed`) | object\_story\_spec.link\_data |
| Video Feed (`meta_video_feed`) | object\_story\_spec.video\_data |
| Existing Post (`meta_existing_post`) | authorized existing Page post |
| Stories (`meta_stories`) | vertical video creative + story placement |
| Reels (`meta_reels`) | vertical video creative + reels placement |
| Carousel (`meta_carousel`) | object\_story\_spec.link\_data.child\_attachments |
| Promoted Offerings (`meta_promoted_offerings`) | one ad set and creative per offering |
| Generated Image 1x1 (`meta_generated_image_1x1`) | generated square image |
| Generated Stories 9x16 (`meta_generated_stories_9x16`) | generated vertical image |
| Generated Video Feed (`meta_generated_video_feed`) | generated feed video |
| Generated Reels 9x16 (`meta_generated_reels_9x16`) | generated vertical video |
| Generated Offerings (`meta_generated_offerings`) | generated image per offering |
| Targeting choice | Provider field |
| - | - |
| Geo Countries (`geo_countries`) | `geo_locations.countries` |
| Geo Regions (`geo_regions`) | `ISO subdivision → provider region key with provenance` |
| Geo Places (`geo_places`) | `Negotiated AdCP 3.2 stable provider city IDs; seller-wide advertisement awaits the authenticated resolver endpoint` |
| Geo Postal Areas (`geo_postal_areas`) | `geo_locations.zips` |
| Ext (`ext`) | `Pre-3.2 ext.scope3.geo_places city compatibility only` |
| Age Ranges (`age_ranges`) | `age_min + age_max` |
| User Age Unknown (`user_age_unknown`) | `Explicit unknown-age policy` |
| Genders (`genders`) | `genders` |
| Device Type (`device_type`) | `device_platforms` |
| Audience Include (`audience_include`) | `custom_audiences` |
| Audience Exclude (`audience_exclude`) | `excluded_custom_audiences` |
| Signal Targeting Groups (`signal_targeting_groups`) | `Creation supports an outer all expression with binary-true Facebook data-provider signals, at most one any group, and zero or more none groups. The adapter maps any to flexible_spec and none to excluded_custom_audiences. All other expressions are refused with UNSUPPORTED_TARGETING_DIMENSION before Meta is called. Updates will be available in a later release.` |
| Frequency Cap (`frequency_cap`) | `Provider frequency controls passed through \`targeting\_overlay.frequency\_cap\` as \`frequency\_control\_specs\` on the ad set. For the Awareness (REACH) product, Meta applies a platform default of 2 impressions per 7 days when no cap is provided; pass \`targeting\_overlay.frequency\_cap\` explicitly to override.\` |
| Placement Selection (`placement_selection`) | `Selecting Facebook Feed and/or Instagram Feed together with Stories and/or Reels is fully honored; selecting only Stories and/or Reels without either Feed is not yet available on every account.` |
| Apostra output | Meta Ads source |
| - | - |
| Impressions (`impressions`) | `impressions` |
| Clicks (`clicks`) | `outbound_clicks` |
| Spend (`spend`) | `spend` |
| Reach (`reach`) | `reach` |
| Conversions (`conversions`) | `actions` |
| Completed Views (`completed_views`) | `video_thruplay_watched_actions` |
| Breakdowns (`breakdowns`) | `publisher_platform + platform_position \| device \| country/region` |
## Provider documentation
* [Meta Marketing API overview](https://developers.facebook.com/docs/marketing-apis/overview)
* [Meta Campaign reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign-group)
* [Meta Ad Set reference](https://developers.facebook.com/docs/marketing-api/reference/ad-campaign)
* [Meta Insights API](https://developers.facebook.com/docs/marketing-api/insights)
* [Meta Business SDK preview surface](https://github.com/facebook/facebook-python-business-sdk/blob/main/facebook_business/adobjects/adcreative.py)
# OpenAI Ads compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/openai-ads
What you can and cannot do with OpenAI Ads through Apostra.
This page compares the OpenAI Ads capabilities represented in the
Apostra adapter with what Apostra currently makes available to buyers.
It answers a practical question: **what can I do through Apostra today, and
what would still require another path?**
**Last reviewed 2026-08-21.** This is Apostra's description of
its own adapter, based on the implementation and the linked provider
documentation. OpenAI Ads has not reviewed or endorsed this page.
Availability can still vary by account permissions, market, and provider access.
## At a glance
| Area | 🟢 Available | 🟡 Partial | 🔵 Testing | ⚪ Planned |
| - | - | - | - | - |
| Campaign products | **6** | **0** | **0** | **0** |
| Creative formats | **2** | **0** | **0** | **0** |
| Adapter capabilities | **10** | **0** | **0** | **6** |
### How to read availability
* 🟢 **Available** — Offered through Apostra now, subject to your ad account's permissions and eligibility with the provider.
* 🟡 **Partial** — Some of the workflow is available; the Comments column explains the boundary.
* 🔵 **Testing** — Apostra is validating the workflow before offering it broadly.
* ⚪ **Planned** — Not offered through Apostra yet. The provider may still support it directly; Comments explains why and links a roadmap reference.
## Roadmap
The adapter roadmap is tracked in AI-6371 (a plain-text reference — Apostra's internal issue tracker is not public).
This roadmap tracks OpenAI Ads product completeness, partner feedback, and live readiness. Every row that is not currently available shows
that reference or a more specific one in its References column, so customers
and partners can quote a stable identifier when asking about a capability.
## Connection, accounts, and identity
OpenAI Ads connects to Apostra through OAuth or an account-scoped API key. Connecting authorizes Apostra to reach your Advertiser account (each OAuth grant or API key is scoped to one advertiser), which you then map to your advertisers. The mapped account decides which OpenAI Ads tenant a buy runs on: a media buy for an unmapped advertiser fails closed with `account_mapping_required` rather than silently using another account. Connection, account-discovery, and revocation behavior is the same across ad platforms — see [Platform accounts and advertiser mapping](/v2/concepts/ad-platform-accounts) for the full model and the [OpenAI Ads section of the connecting guide](/v2/guides/connecting-ad-platforms#openai-ads) for OpenAI Ads-specific permissions.
| Apostra concept | OpenAI Ads concept | What it represents |
| - | - | - |
| Account (`account`) | ad\_account | One OAuth grant or API key resolves to one OpenAI ad account and must match the selected connection. |
| Media Buy (`media_buy`) | campaign | Objective, lifetime budget, status, flight, and top-level targeting container. |
| Package (`package`) | ad\_group | Bid, billing, context, audiences, product selection, and package delivery. |
| Creative Assignment (`creative_assignment`) | ad | Binds a chat card or product template to an ad group. |
| Creative (`creative`) | file + ad creative payload | Chat cards upload a durable image; product templates reuse feed-owned image and destination data. |
## Campaign products
| Feature | Availability | References | Comments |
| - | - | - | - |
| Chat Card Impressions | 🟢 Available | [OpenAI campaigns](https://developers.openai.com/ads/api-reference/campaigns) | Available through Apostra. |
| Chat Card Clicks | 🟢 Available | [OpenAI campaigns](https://developers.openai.com/ads/api-reference/campaigns) | Available through Apostra. |
| Chat Card Conversions | 🟢 Available | [OpenAI campaigns](https://developers.openai.com/ads/api-reference/campaigns) | Available through Apostra. |
| Product Feed Impressions | 🟢 Available | [OpenAI campaigns](https://developers.openai.com/ads/api-reference/campaigns) | Available through Apostra. |
| Product Feed Clicks | 🟢 Available | [OpenAI campaigns](https://developers.openai.com/ads/api-reference/campaigns) | Available through Apostra. |
| Product Feed Conversions | 🟢 Available | [OpenAI campaigns](https://developers.openai.com/ads/api-reference/campaigns) | Available through Apostra. |
## Creative formats
These rows describe **Apostra availability**, not a claim that the ad platform
itself prohibits an unavailable format.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Chat Card | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | Available through Apostra. |
| Product Ad Template | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | Available through Apostra. |
## What the adapter can do
| Feature | Availability | References | Comments |
| - | - | - | - |
| List Accounts | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | Available through Apostra. |
| Sync Accounts | ⚪ Planned | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) · AI-6371 | Account synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Products | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | Six governed objective × creative product mappings |
| Sync Creatives | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | POST /files and typed creative validation |
| List Creatives | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | Durable Apostra creative identities plus provider readback |
| Preview Creative | ⚪ Planned | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) · AI-6371 | OpenAI's [Ads reference](https://developers.openai.com/ads/api-reference/ads) names an expiring ad-preview operation, but its published [OpenAPI schema](https://developers.openai.com/ads/openapi.json) does not define the operation or a response. Apostra therefore cannot verify a public HTTPS preview URL or its exact expiry timestamp and does not offer this capability. Current boundary: both Apostra's implementation and the ad platform's API or account eligibility. |
| Create Media Buy | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | Available through Apostra. |
| Get Media Buys | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | Paginated campaign hierarchy reconstruction |
| Update Media Buy | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | Governed campaign, ad-group, ad, targeting, bid, status, and archive updates |
| Get Media Buy Delivery | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | Campaign insights with package, creative, product, country, and device projections |
| Get Signals | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | Account-owned custom audience discovery and readiness |
| Sync Audiences | ⚪ Planned | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) · AI-6371 | No governed provider member-delta lifecycle is available. Current boundary: Apostra implementation/testing. |
| Sync Event Sources | 🟢 Available | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) | Conversion event-setting discovery and verification |
| Log Event | ⚪ Planned | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) · AI-6371 | Conversions API secrets remain outside the buyer-facing adapter. Current boundary: Apostra implementation/testing. |
| Sync Catalogs | ⚪ Planned | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) · AI-6371 | Catalog synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Account Financials | ⚪ Planned | [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart) · AI-6371 | Account financial reporting is not implemented. Current boundary: Apostra implementation/testing. |
## Placement choices
Apostra does not expose a separate OpenAI Ads placement selector.
## Targeting
| Feature | Availability | References | Comments |
| - | - | - | - |
| Geo Countries | 🟢 Available | [OpenAI campaign targeting](https://developers.openai.com/ads/campaign-targeting) | Country IDs resolved through account-scoped geo lookup |
| Geo Regions | 🟢 Available | [OpenAI campaign targeting](https://developers.openai.com/ads/campaign-targeting) | ISO 3166-2 regions resolved through account-scoped geo lookup |
| Dma | 🟢 Available | [OpenAI campaign targeting](https://developers.openai.com/ads/campaign-targeting) | Nielsen DMA IDs where OpenAI exposes them |
| Context Hints | 🟢 Available | [OpenAI campaign targeting](https://developers.openai.com/ads/campaign-targeting) | Up to 100 canonicalized ad-group context hints |
| Audience Include | 🟢 Available | [OpenAI campaign targeting](https://developers.openai.com/ads/campaign-targeting) | Ready account-owned custom audience IDs |
| Audience Exclude | 🟢 Available | [OpenAI campaign targeting](https://developers.openai.com/ads/campaign-targeting) | Ready account-owned custom audience IDs |
| Custom Audience Bid Multipliers | 🟢 Available | [OpenAI campaign targeting](https://developers.openai.com/ads/campaign-targeting) | Provider micros values with exact ad-group readback |
| Product Set | 🟢 Available | [OpenAI campaign targeting](https://developers.openai.com/ads/campaign-targeting) | One typed filter per supported feed field |
## Reporting
Apostra reports OpenAI Ads delivery day by day (there is no hourly delivery breakdown), aggregated nightly from adapter-reported delivery, in the advertiser's home currency using the booked or historical exchange rate — any cross-currency rollup is always labeled an estimate and is never used for billing. See [Reporting overview](/v2/guides/reporting-overview) for the full model. Apostra does not currently publish an attribution-finality window for OpenAI Ads; the table below lists every metric the adapter surfaces today.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Impressions | 🟢 Available | [OpenAI insights](https://developers.openai.com/ads/api-reference/insights) | Campaign, package, creative, and optional segment totals |
| Clicks | 🟢 Available | [OpenAI insights](https://developers.openai.com/ads/api-reference/insights) | Campaign, package, creative, and optional segment totals |
| Spend | 🟢 Available | [OpenAI insights](https://developers.openai.com/ads/api-reference/insights) | Provider-authoritative currency amount |
| Conversions | 🟢 Available | [OpenAI insights](https://developers.openai.com/ads/api-reference/insights) | Canonical conversions exclude view-through attribution |
| Ext.Openai Ads.View Through Conversions | 🟢 Available | [OpenAI insights](https://developers.openai.com/ads/api-reference/insights) | Reporting-only provider metric |
| Breakdowns | 🟢 Available | [OpenAI insights](https://developers.openai.com/ads/api-reference/insights) | Hourly, daily, or monthly subject to provider combination rules |
## Webhooks and async lifecycle
OpenAI Ads does not push status, delivery, or reporting events into Apostra — every campaign product declares `supports_webhooks: false`.
* **Status.** Apostra does not currently poll OpenAI Ads in the background for creative or media-buy status changes; status reflects what the adapter returned on your last call.
* **Delivery.** Apostra fetches delivery numbers (spend, impressions, clicks) live from OpenAI Ads on demand, only when you call `get_media_buy_delivery` directly. The `/reporting/metrics` endpoints never call the adapter either way — they read from a separately-populated store (see Reporting below), so a reporting request does not itself refresh provider delivery.
This is separate from Apostra's own buyer webhook subscriptions — an unrelated account-level event feed (currently `discovery.revision` only), documented in [Buyer webhooks](/v2/buyer/webhooks).
## Market and account availability
Apostra does not promise that every feature is enabled for every account or
market. The current planning list includes `AU`, `BR`, `CA`, `JP`, `KR`, `MX`, `NZ`, `GB`, `US`; your selected account still determines final availability.
## Important limitations
* **the ad platform's API.** Initial product-feed connection and full uploads must be completed in OpenAI Ads Manager or by SFTP; the API does not perform that setup.
* **the ad platform's API.** OpenAI Ads does not expose a placement selector, so Apostra does not offer one.
* **the ad platform's API or your account's eligibility.** Market and geography availability is determined by the selected OpenAI Ads account at request time.
* **both Apostra's implementation and the ad platform's API or account eligibility.** OpenAI's Ads reference names an expiring ad-preview operation, but its published OpenAPI schema does not define the operation or response, and Apostra has no verified public HTTPS preview URL to return. The account preview URL is only a favicon preview, and Apostra does not substitute it or an approximate render.
* **Apostra implementation/testing.** Apostra has not yet completed end-to-end account validation for these workflows.
## Technical mapping
Engineering-level detail — raw provider objectives, billing enums, optimization
codes, and object-field mappings. Not required reading to understand what
Apostra can do with OpenAI Ads; see the tables above for that.
| Product | Provider objective | Optimization | Billing |
| - | - | - | - |
| Chat Card Impressions (`openai_ads_chat_card_impressions`) | `standard` | `impressions` | `impressions` |
| Chat Card Clicks (`openai_ads_chat_card_clicks`) | `standard` | `clicks` | `clicks` |
| Chat Card Conversions (`openai_ads_chat_card_conversions`) | `standard` | `conversions` | `clicks` |
| Product Feed Impressions (`openai_ads_product_feed_impressions`) | `product_feed` | `impressions` | `impressions` |
| Product Feed Clicks (`openai_ads_product_feed_clicks`) | `product_feed` | `clicks` | `clicks` |
| Product Feed Conversions (`openai_ads_product_feed_conversions`) | `product_feed` | `conversions` | `clicks` |
| Format | OpenAI Ads shape |
| - | - |
| Chat Card (`openai_ads_chat_card`) | chat\_card |
| Product Ad Template (`openai_ads_product_ad_template`) | product\_ad\_template |
| Targeting choice | Provider field |
| - | - |
| Geo Countries (`geo_countries`) | `Country IDs resolved through account-scoped geo lookup` |
| Geo Regions (`geo_regions`) | `ISO 3166-2 regions resolved through account-scoped geo lookup` |
| Dma (`dma`) | `Nielsen DMA IDs where OpenAI exposes them` |
| Context Hints (`context_hints`) | `Up to 100 canonicalized ad-group context hints` |
| Audience Include (`audience_include`) | `Ready account-owned custom audience IDs` |
| Audience Exclude (`audience_exclude`) | `Ready account-owned custom audience IDs` |
| Custom Audience Bid Multipliers (`custom_audience_bid_multipliers`) | `Provider micros values with exact ad-group readback` |
| Product Set (`product_set`) | `One typed filter per supported feed field` |
| Apostra output | OpenAI Ads source |
| - | - |
| Impressions (`impressions`) | `impressions` |
| Clicks (`clicks`) | `clicks` |
| Spend (`spend`) | `spend` |
| Conversions (`conversions`) | `click_through_conversions` |
| Ext.Openai Ads.View Through Conversions (`ext.openai_ads.view_through_conversions`) | `view_through_conversions` |
| Breakdowns (`breakdowns`) | `product \| country \| device` |
## Provider documentation
* [OpenAI Ads API quickstart](https://developers.openai.com/ads/api-quickstart)
* [OpenAI Ads OpenAPI](https://developers.openai.com/ads/openapi.json)
* [OpenAI Ad Account](https://developers.openai.com/ads/api-reference/ad-account)
* [OpenAI campaigns](https://developers.openai.com/ads/api-reference/campaigns)
* [OpenAI ads](https://developers.openai.com/ads/api-reference/ads)
* [OpenAI insights](https://developers.openai.com/ads/api-reference/insights)
* [OpenAI campaign targeting](https://developers.openai.com/ads/campaign-targeting)
* [OpenAI product feeds](https://developers.openai.com/ads/product-feeds)
# Pinterest Ads compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/pinterest
What you can and cannot do with Pinterest Ads through Apostra.
This page compares the Pinterest Ads capabilities represented in the
Apostra adapter with what Apostra currently makes available to buyers.
It answers a practical question: **what can I do through Apostra today, and
what would still require another path?**
**Last reviewed 2026-08-28.** This is Apostra's description of
its own adapter, based on the implementation and the linked provider
documentation. Pinterest Ads has not reviewed or endorsed this page.
Availability can still vary by account permissions, market, and provider access.
## At a glance
| Area | 🟢 Available | 🟡 Partial | 🔵 Testing | ⚪ Planned |
| - | - | - | - | - |
| Campaign products | **7** | **0** | **0** | **2** |
| Creative formats | **3** | **0** | **1** | **9** |
| Adapter capabilities | **10** | **3** | **0** | **3** |
### How to read availability
* 🟢 **Available** — Offered through Apostra now, subject to your ad account's permissions and eligibility with the provider.
* 🟡 **Partial** — Some of the workflow is available; the Comments column explains the boundary.
* 🔵 **Testing** — Apostra is validating the workflow before offering it broadly.
* ⚪ **Planned** — Not offered through Apostra yet. The provider may still support it directly; Comments explains why and links a roadmap reference.
## Roadmap
The adapter roadmap is tracked in AI-1941 (a plain-text reference — Apostra's internal issue tracker is not public).
This roadmap tracks Pinterest adapter product completeness, provider feedback, and live readiness. Every row that is not currently available shows
that reference or a more specific one in its References column, so customers
and partners can quote a stable identifier when asking about a capability.
## Connection, accounts, and identity
Pinterest Ads connects to Apostra through an OAuth grant. Connecting authorizes Apostra to reach your Ad account, which you then map to your advertisers. The mapped account decides which Pinterest Ads tenant a buy runs on: a media buy for an unmapped advertiser fails closed with `account_mapping_required` rather than silently using another account. Connection, account-discovery, and revocation behavior is the same across ad platforms — see [Platform accounts and advertiser mapping](/v2/concepts/ad-platform-accounts) for the full model and the [Pinterest Ads section of the connecting guide](/v2/guides/connecting-ad-platforms#pinterest) for Pinterest Ads-specific permissions.
| Apostra concept | Pinterest Ads concept | What it represents |
| - | - | - |
| Account (`account`) | ad account | Explicit Pinterest ad account selected after OAuth. |
| Media Buy (`media_buy`) | campaign | Objective, intended promotion type, lifecycle, and budget cap. |
| Package (`package`) | ad group | Budget, bid, optimization, targeting, placement group, frequency, and flight. |
| Creative (`creative`) | Pin | Existing or adapter-published image/video Pin. |
| Creative Assignment (`creative_assignment`) | ad | Ad binds a Pin to an ad group. |
| Signal (`signal`) | audience or interest node | Account-owned audiences and provider taxonomy IDs. |
## Campaign products
| Feature | Availability | References | Comments |
| - | - | - | - |
| Awareness | 🟢 Available | [Pinterest campaigns and ad groups](https://developers.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/) | Available through Apostra. |
| Consideration | 🟢 Available | [Pinterest campaigns and ad groups](https://developers.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/) | Available through Apostra. |
| Video Views | 🟢 Available | [Pinterest campaigns and ad groups](https://developers.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/) | Available through Apostra. |
| Conversions | 🟢 Available | [Pinterest campaigns and ad groups](https://developers.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/) | Available through Apostra. |
| Leads | 🟢 Available | [Pinterest campaigns and ad groups](https://developers.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/) | Available through Apostra. |
| Traffic | 🟢 Available | [Pinterest campaigns and ad groups](https://developers.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/) | Available through Apostra. |
| Performance Plus | 🟢 Available | [Pinterest campaigns and ad groups](https://developers.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/) | Available through Apostra. |
| Catalog Sales | ⚪ Planned | [Pinterest campaigns and ad groups](https://developers.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/) · AI-3795 | Apostra has not completed the end-to-end catalog sales workflow. Current boundary: Apostra implementation/testing. |
| Idea Pins | ⚪ Planned | [Pinterest campaigns and ad groups](https://developers.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/) · AI-3797 | Apostra does not yet support creating and reading Idea Pin creative end to end. Current boundary: Apostra implementation/testing. |
## Creative formats
These rows describe **Apostra availability**, not a claim that the ad platform
itself prohibits an unavailable format.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Existing Post | 🔵 Testing | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-4032 | Apostra is validating the authorization and promotion lifecycle for advertiser-owned Pins before making it generally available. Current boundary: Apostra implementation/testing. |
| Standard Pin | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Available through Apostra. |
| Video Pin | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Available through Apostra. |
| Max Width Video | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Available through Apostra. |
| Carousel | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-4032 | Apostra has not completed multi-image card publication, ad assignment, and Carousel readback. Current boundary: Apostra implementation/testing. |
| Collection | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-4032 | Apostra has not completed the hero, product-tile, catalog-linkage, assignment, and Collection readback workflow. Current boundary: Apostra implementation/testing. |
| Idea Pin | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-4032 | Apostra has not completed multi-page image and video publication, assignment, and Idea Pin readback. Current boundary: Apostra implementation/testing. |
| Showcase | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-4032 | Apostra has not completed Showcase card and feature publication, assignment, eligibility checks, and readback. Current boundary: Apostra implementation/testing. |
| Quiz | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-4032 | Apostra has not completed question, answer, result-card publication, account eligibility checks, assignment, and Quiz readback. Current boundary: Apostra implementation/testing. |
| Promoted Offerings | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-4032 | This is an Apostra orchestration format. The per-offering Pin selection, ad-group expansion, assignment, and readback workflow is not complete. Current boundary: Apostra implementation/testing. |
| Generated Image 2x3 | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-4032 | This is an Apostra-generated format, not a separate Pinterest ad type. Apostra has not completed generation or generated-asset intake, faithful preview, Pin publication, ad assignment, replacement, exact readback and reporting, and cleanup as one workflow. The ordinary 2:3 Standard Pin remains available. Current boundary: Apostra implementation/testing. |
| Generated Video | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-4032 | This is an Apostra-generated format, not a separate Pinterest ad type. Apostra has not completed video generation, Pin publication, ad assignment, and readback. Current boundary: Apostra implementation/testing. |
| Generated Offerings | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-4032 | This is an Apostra-generated format, not a separate Pinterest ad type. Apostra has not completed per-offering generation, Pin publication, ad-group assignment, and readback. Current boundary: Apostra implementation/testing. |
## What the adapter can do
| Feature | Availability | References | Comments |
| - | - | - | - |
| List Accounts | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Authorized ad-account discovery |
| Sync Accounts | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-1941 | Account synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Products | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Lists seven available campaign products and two products Apostra does not currently offer. |
| Sync Creatives | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Reserves durable Pin and assignment ownership before Pinterest writes, verifies the exact created Pin, selected-account owner, image or video class, and authored fields, and preserves the original creative identity and format. An accepted video media ID is journaled before upload/readiness polling, so a timeout or downstream source/upload rejection retries that exact media object instead of registering or uploading a duplicate; only terminal Pinterest processing failure permits replacement. The complete media operation has a four-minute deadline below a ten-minute durable lease; recovery renews and later compares the exact lease token, so stale callers cannot finish over a newer owner. Returned timeouts release it immediately, while crashes become recoverable after expiry. Replays join account inventory to an exact Pin point read, so omitted bulk-inventory fields do not create false drift and a field actually removed from the Pin still fails closed. Pinterest does not echo the input image URL or uploaded media ID, so transient rendition URLs and dimensions are not treated as durable identity; the exact requested assets remain bound separately. A missing completed ad is preserved for a durable 15-minute reconciliation window and requires another exact absence before replacement. Reconciliation failures attempt verified removal only when mutation ownership is proven; uncertain durable handoffs preserve the exact provider objects, fail visibly, and alert operators. Changed assets still require a new creative ID. |
| List Creatives | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Full selected-account Pin inventory; feed-version-bound, account-bound cursor pages of at most 100 creatives; filters and sort applied before the bounded page; exact point reads only for materialized identities on that page—including an inventory-omitted identity when its deterministic page is reached—with four concurrent reads under one 30-second deadline; and ad-assignment projection |
| Preview Creative | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Previews a saved Pinterest creative only when it resolves to one completed assignment in the selected ad account and Pinterest readback confirms the same ad group, Pin, ad, and placement group. Apostra returns Pinterest's credential-free HTTPS preview link with Pinterest's expiry and never returns executable embed code. Creating the preview record does not create or change the campaign, ad group, ad, or Pin. Batches accept at most four creatives so every accepted item starts within the bounded 30-second execution window. Unsynchronized or transient creatives, ambiguous assignments, unsupported formats, and identity or placement mismatches fail closed; no reference-renderer fallback is advertised. |
| Create Media Buy | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Campaign → ad group → ad |
| Get Media Buys | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Campaign hierarchy and targeting provenance |
| Update Media Buy | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Lifecycle, budget, bid, geography, audiences, and creative assignment updates with readback |
| Get Media Buy Delivery | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Campaign and ad-group analytics |
| Get Signals | 🟢 Available | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) | Audience and interest discovery |
| Sync Audiences | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-1941 | Apostra has not made customer-list audience syncing publicly available while it completes the account-bound lifecycle and deletion safeguards. Current boundary: Apostra implementation/testing. |
| Sync Event Sources | 🟡 Partial | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-1941 | Apostra discovers the selected account's Pinterest Conversion Tags and returns Pinterest-issued installation code when available. It does not create, change, or delete Tags, and server-side event delivery is not publicly available. Current boundary: Apostra implementation/testing. |
| Log Event | ⚪ Planned | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-1941 | Apostra has not made server-side event ingestion publicly available. It is still validating end-to-end delivery against a Pinterest account set aside for testing. Nothing to change on your side. Current boundary: Apostra implementation/testing. |
| Sync Catalogs | 🟡 Partial | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-1941 | Registers a Pinterest catalog feed URL through POST /catalogs/feeds and the local store; inline item push is not implemented. Current boundary: Apostra implementation/testing. |
| Get Account Financials | 🟡 Partial | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-1941 | Campaign spend totals are available; the newer account-financials path is not yet connected. Current boundary: Apostra implementation/testing. |
## Placement choices
| Feature | Availability | References | Comments |
| - | - | - | - |
| Pinterest Feed | 🟡 Partial | [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/) · AI-1941 | Apostra sends all eligible Pinterest placements together; it does not expose a per-placement selector. Current boundary: Apostra implementation/testing. |
## Targeting
| Feature | Availability | References | Comments |
| - | - | - | - |
| Geo Countries | 🟢 Available | [Pinterest targeting](https://developers.pinterest.com/docs/work-with-targets-and-audiences/set-up-targets-for-your-ads/) | Available through Apostra. |
| Geo Regions | 🟢 Available | [Pinterest targeting](https://developers.pinterest.com/docs/work-with-targets-and-audiences/set-up-targets-for-your-ads/) | Available through Apostra. |
| Audience Include | ⚪ Planned | [Pinterest targeting](https://developers.pinterest.com/docs/work-with-targets-and-audiences/set-up-targets-for-your-ads/) · AI-1941 | Apostra does not yet offer customer-list audience inclusion for Pinterest campaigns. Current boundary: Apostra implementation/testing. |
| Audience Exclude | ⚪ Planned | [Pinterest targeting](https://developers.pinterest.com/docs/work-with-targets-and-audiences/set-up-targets-for-your-ads/) · AI-1941 | Apostra does not yet offer customer-list audience exclusion for Pinterest campaigns. Current boundary: Apostra implementation/testing. |
| Signals | 🟢 Available | [Pinterest targeting](https://developers.pinterest.com/docs/work-with-targets-and-audiences/set-up-targets-for-your-ads/) | Available through Apostra. |
| Frequency Cap | 🟢 Available | [Pinterest targeting](https://developers.pinterest.com/docs/work-with-targets-and-audiences/set-up-targets-for-your-ads/) | Available through Apostra. |
| Placement Refs | 🟡 Partial | [Pinterest targeting](https://developers.pinterest.com/docs/work-with-targets-and-audiences/set-up-targets-for-your-ads/) · AI-1941 | Apostra does not expose individual Pinterest placement selection; campaigns use all eligible placements. Current boundary: Apostra implementation/testing. |
## Reporting
Apostra reports Pinterest Ads delivery day by day (there is no hourly delivery breakdown), aggregated nightly from adapter-reported delivery, in the advertiser's home currency using the booked or historical exchange rate — any cross-currency rollup is always labeled an estimate and is never used for billing. See [Reporting overview](/v2/guides/reporting-overview) for the full model. Apostra does not currently publish an attribution-finality window for Pinterest Ads; the table below lists every metric the adapter surfaces today.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Impressions | 🟢 Available | [Pinterest analytics](https://developers.pinterest.com/docs/work-with-analytics/overview/) | Campaign and ad-group totals |
| Clicks | 🟢 Available | [Pinterest analytics](https://developers.pinterest.com/docs/work-with-analytics/overview/) | Campaign and ad-group totals |
| Spend | 🟢 Available | [Pinterest analytics](https://developers.pinterest.com/docs/work-with-analytics/overview/) | Provider micro-currency converted exactly |
| Ctr | 🟢 Available | [Pinterest analytics](https://developers.pinterest.com/docs/work-with-analytics/overview/) | Calculated from raw provider values |
| Views | 🟢 Available | [Pinterest analytics](https://developers.pinterest.com/docs/work-with-analytics/overview/) | Video-view campaigns |
| By Package | 🟢 Available | [Pinterest analytics](https://developers.pinterest.com/docs/work-with-analytics/overview/) | AdCP package projection |
| By Catalog Item | 🟢 Available | [Pinterest analytics](https://developers.pinterest.com/docs/work-with-analytics/overview/) | Returned when the package has catalog-item identity |
## Webhooks and async lifecycle
Pinterest Ads does not push status, delivery, or reporting events into Apostra — every campaign product declares `supports_webhooks: false`.
* **Status.** Apostra does not currently poll Pinterest Ads in the background for creative or media-buy status changes; status reflects what the adapter returned on your last call.
* **Delivery.** Apostra fetches delivery numbers (spend, impressions, clicks) live from Pinterest Ads on demand, only when you call `get_media_buy_delivery` directly. The `/reporting/metrics` endpoints never call the adapter either way — they read from a separately-populated store (see Reporting below), so a reporting request does not itself refresh provider delivery.
This is separate from Apostra's own buyer webhook subscriptions — an unrelated account-level event feed (currently `discovery.revision` only), documented in [Buyer webhooks](/v2/buyer/webhooks).
## Market and account availability
Apostra does not promise that every feature is enabled for every account or
market. Availability is checked against your selected ad account rather than a global market list on this page.
## Important limitations
* **Apostra implementation/testing.** Catalog Sales and Idea Pins are not available through Apostra because their end-to-end creation workflows are incomplete.
* **Apostra implementation/testing.** Promoting an existing Pin is still being tested and is not generally available.
* **Apostra implementation/testing.** Pinterest preview is available only for a saved creative with one verified assignment in the selected ad account. Unsynchronized or transient creatives do not use an approximate fallback.
* **Apostra implementation/testing.** Generated and other rich Pin formats remain planned until asset intake, faithful preview, materialization, assignment, replacement, reverse readback and reporting, and cleanup are complete. Ordinary 2:3 Standard Pins are available.
* **Apostra implementation/testing.** Apostra sends all eligible Pinterest placements together instead of exposing individual placement selection.
* **the ad platform's API.** Pinterest analytics can take up to 72 hours to stabilize, and Pinterest constrains the available attribution windows.
* **Apostra implementation/testing.** Catalog sync accepts feed URLs but does not push inline catalog items.
* **Apostra implementation/testing.** Customer-list audiences and server-side event ingestion are not yet publicly available.
## Technical mapping
Engineering-level detail — raw provider objectives, billing enums, optimization
codes, and object-field mappings. Not required reading to understand what
Apostra can do with Pinterest Ads; see the tables above for that.
| Product | Provider objective | Optimization | Billing |
| - | - | - | - |
| Awareness (`pinterest_awareness`) | `AWARENESS` | `AUTOMATIC_BID` | `IMPRESSION` |
| Consideration (`pinterest_consideration`) | `CONSIDERATION` | `clicks` | `CLICKTHROUGH` |
| Video Views (`pinterest_video_views`) | `VIDEO_VIEW` | `AUTOMATIC_BID` | `VIDEO_V_50_MRC` |
| Conversions (`pinterest_conversions`) | `SALES / STANDARD_AD` | `CHECKOUT` | `IMPRESSION` |
| Leads (`pinterest_leads`) | `LEADS / STANDARD_AD` | `LEAD` | `IMPRESSION` |
| Traffic (`pinterest_traffic`) | `WEB_SESSIONS` | `clicks` | `CLICKTHROUGH` |
| Performance Plus (`pinterest_performance_plus`) | `SALES / STANDARD_AD` | `CHECKOUT + auto targeting` | `IMPRESSION` |
| Catalog Sales (`pinterest_catalog_sales`) | `SALES / CATALOG` | `CHECKOUT` | `IMPRESSION` |
| Idea Pins (`pinterest_idea_pins`) | `AWARENESS` | `AUTOMATIC_BID` | `IMPRESSION` |
| Format | Pinterest Ads shape |
| - | - |
| Existing Post (`pinterest_existing_post`) | authorized existing Pin |
| Standard Pin (`pinterest_standard_pin`) | REGULAR image Pin |
| Video Pin (`pinterest_video_pin`) | VIDEO Pin |
| Max Width Video (`pinterest_max_width_video`) | MAX\_VIDEO Pin |
| Carousel (`pinterest_carousel`) | CAROUSEL Pin |
| Collection (`pinterest_collection`) | COLLECTION Pin |
| Idea Pin (`pinterest_idea_pin`) | IDEA Pin |
| Showcase (`pinterest_showcase`) | SHOWCASE Pin |
| Quiz (`pinterest_quiz`) | QUIZ Pin |
| Promoted Offerings (`pinterest_promoted_offerings`) | one ad group and Pin per offering |
| Generated Image 2x3 (`pinterest_generated_image_2x3`) | generated 2:3 Pin image |
| Generated Video (`pinterest_generated_video`) | generated vertical Pin video |
| Generated Offerings (`pinterest_generated_offerings`) | generated Pin per offering |
| Targeting choice | Provider field |
| - | - |
| Geo Countries (`geo_countries`) | `targeting_spec.LOCATION` |
| Geo Regions (`geo_regions`) | `ISO subdivision → targeting_spec.GEO with provenance` |
| Audience Include (`audience_include`) | `Not publicly activated while the governed audience lifecycle and targeting authority remain pending` |
| Audience Exclude (`audience_exclude`) | `Not publicly activated while the governed audience lifecycle and targeting authority remain pending` |
| Signals (`signals`) | `targeting_spec.INTEREST after taxonomy resolution` |
| Frequency Cap (`frequency_cap`) | `lifetime_frequency_cap` |
| Placement Refs (`placement_refs`) | `No per-placement AdCP selection; ad groups currently use placement_group ALL` |
| Apostra output | Pinterest Ads source |
| - | - |
| Impressions (`impressions`) | `TOTAL_IMPRESSION` |
| Clicks (`clicks`) | `TOTAL_CLICKTHROUGH` |
| Spend (`spend`) | `SPEND_IN_MICRO_DOLLAR` |
| Ctr (`ctr`) | `TOTAL_CLICKTHROUGH / TOTAL_IMPRESSION` |
| Views (`views`) | `TOTAL_VIDEO_MRC_VIEWS` |
| By Package (`by_package`) | `ad-group analytics` |
| By Catalog Item (`by_catalog_item`) | `durable entity mapping` |
## Provider documentation
* [Pinterest Ads API overview](https://developers.pinterest.com/docs/api/v5/)
* [Pinterest campaigns and ad groups](https://developers.pinterest.com/docs/work-with-ads/create-campaigns-and-ad-groups/)
* [Pinterest ads](https://developers.pinterest.com/docs/work-with-ads/create-ads/)
* [Pinterest shareable ad previews](https://developers.pinterest.com/docs/work-with-ads/generate-shareable-ad-previews/)
* [Pinterest targeting](https://developers.pinterest.com/docs/work-with-targets-and-audiences/set-up-targets-for-your-ads/)
* [Pinterest analytics](https://developers.pinterest.com/docs/work-with-analytics/overview/)
# Reddit Ads compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/reddit
What you can and cannot do with Reddit Ads through Apostra.
This page compares the Reddit Ads capabilities represented in the
Apostra adapter with what Apostra currently makes available to buyers.
It answers a practical question: **what can I do through Apostra today, and
what would still require another path?**
**Last reviewed 2026-08-18.** This is Apostra's description of
its own adapter, based on the implementation and the linked provider
documentation. Reddit Ads has not reviewed or endorsed this page.
Availability can still vary by account permissions, market, and provider access.
## At a glance
| Area | 🟢 Available | 🟡 Partial | 🔵 Testing | ⚪ Planned |
| - | - | - | - | - |
| Campaign products | **0** | **0** | **5** | **3** |
| Creative formats | **0** | **0** | **0** | **11** |
| Adapter capabilities | **1** | **0** | **0** | **15** |
### How to read availability
* 🟢 **Available** — Offered through Apostra now, subject to your ad account's permissions and eligibility with the provider.
* 🟡 **Partial** — Some of the workflow is available; the Comments column explains the boundary.
* 🔵 **Testing** — Apostra is validating the workflow before offering it broadly.
* ⚪ **Planned** — Not offered through Apostra yet. The provider may still support it directly; Comments explains why and links a roadmap reference.
## Roadmap
The adapter roadmap is tracked in AI-1943 (a plain-text reference — Apostra's internal issue tracker is not public).
This roadmap tracks Reddit adapter product completeness, sandbox readiness, and live readiness. Every row that is not currently available shows
that reference or a more specific one in its References column, so customers
and partners can quote a stable identifier when asking about a capability.
## Connection, accounts, and identity
Reddit Ads connects to Apostra through an OAuth grant. Connecting a Business account gives Apostra access to each Ad account it can reach — you then map each one to your advertisers. The mapped account decides which Reddit Ads tenant a buy runs on: a media buy for an unmapped advertiser fails closed with `account_mapping_required` rather than silently using another account. Connection, account-discovery, and revocation behavior is the same across ad platforms — see [Platform accounts and advertiser mapping](/v2/concepts/ad-platform-accounts) for the full model and the [Reddit Ads section of the connecting guide](/v2/guides/connecting-ad-platforms#reddit) for Reddit Ads-specific permissions.
| Apostra concept | Reddit Ads concept | What it represents |
| - | - | - |
| Account (`account`) | provider advertising account | OAuth resolves the selected Ad account through its owning Business; that exact account is the execution boundary. |
| Media Buy (`media_buy`) | campaign | Media Buy maps to campaign. |
| Package (`package`) | ad group | Budget, targeting, optimization, and flight bindings. |
| Creative Assignment (`creative_assignment`) | promoted post or creative | Creative materialization or assignment must use an authorized business profile; published-post reuse additionally requires exact source-community ownership and eligibility plus provider readback. |
## Campaign products
| Feature | Availability | References | Comments |
| - | - | - | - |
| Promoted Post Awareness | 🔵 Testing | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Traffic | 🔵 Testing | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Conversion-Intent Traffic | 🔵 Testing | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Traffic | 🔵 Testing | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Community | 🔵 Testing | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Image Awareness | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Video Awareness | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Carousel | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
## Creative formats
These rows describe **Apostra availability**, not a claim that the ad platform
itself prohibits an unavailable format.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Image Feed | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Image Conversation | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Video Feed | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Video Conversation | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Carousel | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Freeform | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Existing Post | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Promoted Offerings | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Image | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Video | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Offerings | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
## What the adapter can do
| Feature | Availability | References | Comments |
| - | - | - | - |
| List Accounts | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Accounts | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Account synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Products | 🟢 Available | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) | An empty brief returns five deterministic server-owned products with provider-flow contracts; provider lifecycle eligibility is not inferred from discovery. |
| Sync Creatives | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| List Creatives | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Preview Creative | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Reddit previews require a saved ad and an initial link generated manually in Ads Manager. Apostra does not yet verify and return that selected-account link and expiry, and Reddit has no transient preview path. Current boundary: Apostra implementation/testing. |
| Create Media Buy | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Media Buys | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Update Media Buy | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Media Buy Delivery | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Signals | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Audiences | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Event Sources | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Log Event | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Catalogs | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Account Financials | ⚪ Planned | [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup) · AI-1943 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
## Placement choices
| Feature | Availability | References | Comments |
| - | - | - | - |
| Reddit Feed | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra does not yet expose Reddit Feed as a separately selectable and verifiable placement. Current boundary: Apostra implementation/testing. |
| Reddit Conversation | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra does not yet expose Reddit Conversation as a separately selectable and verifiable placement. Current boundary: Apostra implementation/testing. |
## Targeting
| Feature | Availability | References | Comments |
| - | - | - | - |
| Geo Countries | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Geo Regions | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Signals | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
## Reporting
Apostra reports Reddit Ads delivery day by day (there is no hourly delivery breakdown), aggregated nightly from adapter-reported delivery, in the advertiser's home currency using the booked or historical exchange rate — any cross-currency rollup is always labeled an estimate and is never used for billing. See [Reporting overview](/v2/guides/reporting-overview) for the full model. Apostra does not currently publish an attribution-finality window for Reddit Ads; the table below lists every metric the adapter surfaces today.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Impressions | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Campaign and ad-group totals |
| Spend | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Micros converted to account-currency major units |
| Clicks | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Campaign and ad-group totals |
| Views | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Viewable video impressions; starts are excluded |
| Completed Views | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | 100% video completions |
| Conversions | ⚪ Planned | [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/) · AI-1943 | Key conversion total |
## Webhooks and async lifecycle
Reddit Ads does not push status, delivery, or reporting events into Apostra — every campaign product declares `supports_webhooks: false`.
* **Status.** Apostra's status-callback poller checks pending Reddit Ads creative status every 5 minutes (backing off to hourly as a status goes unchanged) and fires a one-shot **AdCP task-completion webhook** — the `pushNotificationConfig` a buyer can register on some async operations — once a creative reaches a terminal status. That is Apostra notifying the buyer's own system, not Reddit Ads notifying Apostra. Media-buy status is not polled this way for any adapter today.
* **Delivery.** Apostra has not yet made on-demand delivery fetching generally available for Reddit Ads — see Get Media Buy Delivery in "What the adapter can do" above. The `/reporting/metrics` endpoints never call the adapter either way — they read from a separately-populated store (see Reporting below), so a reporting request does not itself refresh provider delivery.
This is separate from Apostra's own buyer webhook subscriptions — an unrelated account-level event feed (currently `discovery.revision` only), documented in [Buyer webhooks](/v2/buyer/webhooks).
## Market and account availability
Apostra does not promise that every feature is enabled for every account or
market. Availability is checked against your selected ad account rather than a global market list on this page.
## Important limitations
* **Apostra implementation/testing.** This page is not yet a confirmed complete catalog of every Reddit Ads creation path.
* **Apostra implementation/testing.** The listed campaign products exist in the current adapter code but are still being certified before general availability.
* **Apostra implementation/testing.** Dynamic Product Ads prospecting and retargeting are unavailable until Apostra implements the complete provider catalog, product-set, Pixel-event, shopping creative, placement, and readback workflow.
* **Apostra implementation/testing.** Current conversion-intent, app, and community-intent products optimize for clicks, not Pixel conversions, purchases, app installs, or community engagement.
* **Apostra implementation/testing.** Provider-rendered previews, separately selectable Reddit placements, provider push webhooks, and generally available published-post reuse are not currently offered.
## Technical mapping
Engineering-level detail — raw provider objectives, billing enums, optimization
codes, and object-field mappings. Not required reading to understand what
Apostra can do with Reddit Ads; see the tables above for that.
| Product | Provider objective | Optimization | Billing |
| - | - | - | - |
| Promoted Post Awareness (`reddit_promoted_post_awareness`) | `IMPRESSIONS` | `provider_default_or_omitted` | `CPM` |
| Traffic (`reddit_traffic`) | `CLICKS` | `CLICKS` | `CPC` |
| Conversion-Intent Traffic (`reddit_conversions`) | `CLICKS` | `CLICKS` | `CPC` |
| App Traffic (`reddit_app_installs`) | `CLICKS` | `CLICKS` | `CPC` |
| Community (`reddit_community`) | `CLICKS` | `CLICKS` | `CPC` |
| Image Awareness (`reddit_image_awareness`) | `IMPRESSIONS` | `provider_default_or_omitted` | `CPM` |
| Video Awareness (`reddit_video_awareness`) | `VIDEO_VIEWABLE_IMPRESSIONS` | `VIDEO_VIEW_6S` | `CPV6` |
| Carousel (`reddit_carousel`) | `CLICKS` | `CLICKS` | `CPC` |
| Format | Reddit Ads shape |
| - | - |
| Image Feed (`image_feed`) | Canonical Code Format |
| Image Conversation (`image_conversation`) | Canonical Code Format |
| Video Feed (`video_feed`) | Canonical Code Format |
| Video Conversation (`video_conversation`) | Canonical Code Format |
| Carousel (`carousel`) | Canonical Code Format |
| Freeform (`freeform`) | Canonical Code Format |
| Existing Post (`reddit_existing_post`) | Canonical Code Format |
| Promoted Offerings (`reddit_promoted_offerings`) | Canonical Code Format |
| Generated Image (`reddit_generated_image`) | Canonical Code Format |
| Generated Video (`reddit_generated_video`) | Canonical Code Format |
| Generated Offerings (`reddit_generated_offerings`) | Canonical Code Format |
| Targeting choice | Provider field |
| - | - |
| Geo Countries (`geo_countries`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Geo Regions (`geo_regions`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Signals (`signals`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Apostra output | Reddit Ads source |
| - | - |
| Impressions (`impressions`) | `IMPRESSIONS` |
| Spend (`spend`) | `SPEND` |
| Clicks (`clicks`) | `CLICKS` |
| Views (`views`) | `VIDEO_VIEWABLE_IMPRESSIONS` |
| Completed Views (`completed_views`) | `VIDEO_WATCHED_100_PERCENT` |
| Conversions (`conversions`) | `KEY_CONVERSION_TOTAL_COUNT` |
## Provider documentation
* [Reddit Ads public documentation](https://ads-api.reddit.com/docs/v3/)
* [Reddit Ads API campaign setup](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/campaign-setup)
* [Reddit Dynamic Product Ads](https://business.reddithelp.com/articles/Knowledge/dynamic-product-ads)
* [Reddit Ads API product catalogs](https://ads-api.reddit.com/docs/v3/api/product-catalogs)
* [Reddit preview an ad](https://ads-api.reddit.com/docs/v3/guides/programs/campaign/preview-an-ad)
* [Reddit Ads API get ad](https://ads-api.reddit.com/docs/v3/api/get-ad)
* [Reddit Ads API update ad](https://ads-api.reddit.com/docs/v3/api/update-ad)
# Snap Ads compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/snap
What you can and cannot do with Snap Ads through Apostra.
This page compares the Snap Ads capabilities represented in the
Apostra adapter with what Apostra currently makes available to buyers.
It answers a practical question: **what can I do through Apostra today, and
what would still require another path?**
**Last reviewed 2026-08-20.** This is Apostra's description of
its own adapter, based on the implementation and the linked provider
documentation. Snap Ads has not reviewed or endorsed this page.
Availability can still vary by account permissions, market, and provider access.
## At a glance
| Area | 🟢 Available | 🟡 Partial | 🔵 Testing | ⚪ Planned |
| - | - | - | - | - |
| Campaign products | **3** | **0** | **0** | **7** |
| Creative formats | **2** | **0** | **1** | **9** |
| Adapter capabilities | **15** | **0** | **0** | **1** |
### How to read availability
* 🟢 **Available** — Offered through Apostra now, subject to your ad account's permissions and eligibility with the provider.
* 🟡 **Partial** — Some of the workflow is available; the Comments column explains the boundary.
* 🔵 **Testing** — Apostra is validating the workflow before offering it broadly.
* ⚪ **Planned** — Not offered through Apostra yet. The provider may still support it directly; Comments explains why and links a roadmap reference.
## Roadmap
The adapter roadmap is tracked in AI-1945 (a plain-text reference — Apostra's internal issue tracker is not public).
This roadmap tracks Snap adapter product completeness, provider access, and live readiness. Every row that is not currently available shows
that reference or a more specific one in its References column, so customers
and partners can quote a stable identifier when asking about a capability.
## Connection, accounts, and identity
Snap Ads connects to Apostra through an OAuth grant. Connecting a Organization account gives Apostra access to each Ad account it can reach — you then map each one to your advertisers. The mapped account decides which Snap Ads tenant a buy runs on: a media buy for an unmapped advertiser fails closed with `account_mapping_required` rather than silently using another account. Connection, account-discovery, and revocation behavior is the same across ad platforms — see [Platform accounts and advertiser mapping](/v2/concepts/ad-platform-accounts) for the full model and the [Snap Ads section of the connecting guide](/v2/guides/connecting-ad-platforms#snap) for Snap Ads-specific permissions.
| Apostra concept | Snap Ads concept | What it represents |
| - | - | - |
| Account (`account`) | ad account | Explicit account selected from the authorized organization. |
| Media Buy (`media_buy`) | campaign | Objective, lifecycle, and campaign spend cap. |
| Package (`package`) | ad squad | Targeting, budget, bid, billing event, optimization, flight, and placement configuration. |
| Creative (`creative`) | media + creative | Uploaded image/video or an authorized existing Snap. |
| Creative Assignment (`creative_assignment`) | ad | Ad links one creative to one ad squad. |
| Signal (`signal`) | audience segment | Account-owned first-party or provider audience segment. |
## Campaign products
| Feature | Availability | References | Comments |
| - | - | - | - |
| Awareness | 🟢 Available | [Snap Campaigns](https://developers.snap.com/marketing-api/Ads-API/campaigns) | Available through Apostra. |
| Traffic | 🟢 Available | [Snap Campaigns](https://developers.snap.com/marketing-api/Ads-API/campaigns) | Available through Apostra. |
| Web Conversions | 🟢 Available | [Snap Campaigns](https://developers.snap.com/marketing-api/Ads-API/campaigns) | Available through Apostra. |
| Catalog Traffic | ⚪ Planned | [Snap Campaigns](https://developers.snap.com/marketing-api/Ads-API/campaigns) · AI-1945 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| App Installs | ⚪ Planned | [Snap Campaigns](https://developers.snap.com/marketing-api/Ads-API/campaigns) · AI-1945 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Story Ads | ⚪ Planned | [Snap Campaigns](https://developers.snap.com/marketing-api/Ads-API/campaigns) · AI-1945 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Collection | ⚪ Planned | [Snap Campaigns](https://developers.snap.com/marketing-api/Ads-API/campaigns) · AI-1945 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Lead Gen | ⚪ Planned | [Snap Campaigns](https://developers.snap.com/marketing-api/Ads-API/campaigns) · AI-1945 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Sponsored Snaps | ⚪ Planned | [Snap Campaigns](https://developers.snap.com/marketing-api/Ads-API/campaigns) · AI-1945 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
| Commercial | ⚪ Planned | [Snap Campaigns](https://developers.snap.com/marketing-api/Ads-API/campaigns) · AI-1945 | Apostra has not completed the end-to-end creative workflow for this product. Current boundary: Apostra implementation/testing. |
## Creative formats
These rows describe **Apostra availability**, not a claim that the ad platform
itself prohibits an unavailable format.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Ad Video 9x16 | 🟢 Available | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) | Available through Apostra. |
| Ad Image 9x16 | 🟢 Available | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) | Available through Apostra. |
| Existing Post | 🔵 Testing | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) · AI-1945 | Apostra is still testing this path, and Snap must allow the selected profile to use it. Current boundary: both Apostra's implementation and the ad platform's API or account eligibility. |
| Story Ad | ⚪ Planned | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) · AI-1945 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Collection Ad | ⚪ Planned | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) · AI-1945 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Lead Gen Ad | ⚪ Planned | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) · AI-1945 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Promoted Offerings | ⚪ Planned | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) · AI-1945 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Image 9x16 | ⚪ Planned | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) · AI-1945 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Offerings | ⚪ Planned | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) · AI-1945 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Sponsored Snap | ⚪ Planned | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) · AI-1945 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Sponsored Snap | ⚪ Planned | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) · AI-1945 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Commercial | ⚪ Planned | [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives) · AI-1945 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
## What the adapter can do
| Feature | Availability | References | Comments |
| - | - | - | - |
| List Accounts | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Organization and ad-account discovery |
| Sync Accounts | ⚪ Planned | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) · AI-1945 | Account synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Products | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Three executable and seven blocked product mappings |
| Sync Creatives | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Media upload, creative creation, and exact identity readback for round-trip formats |
| List Creatives | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Creative/media inventory and assignment projection |
| Preview Creative | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Saved 9:16 image/video creative → selected-account creative and media readback → signed hosted preview link; no transient manifests or placement selection. Batches accept at most four creatives so every accepted item starts inside the bounded four-worker, 30-second aggregate execution window. |
| Create Media Buy | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Campaign → ad squad → ad |
| Get Media Buys | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Campaign hierarchy and targeting provenance |
| Update Media Buy | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Lifecycle, budget, bid, geography, audiences, and creative assignments with readback |
| Get Media Buy Delivery | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Campaign/ad-squad stats timeseries |
| Get Signals | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Governed targeting segment discovery |
| Sync Audiences | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Audience create/replace/status lifecycle |
| Sync Event Sources | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Snap Pixel discovery and readiness |
| Log Event | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Conversions API event ingestion |
| Sync Catalogs | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Catalog synchronization with tenant isolation |
| Get Account Financials | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Currency and account funding context |
## Placement choices
| Feature | Availability | References | Comments |
| - | - | - | - |
| Snapchat full-screen inventory | 🟡 Partial | [Snap Ad Squads](https://developers.snap.com/marketing-api/Ads-API/ad-squads) · AI-1945 | Placement choice is only partially available through Apostra. Current boundary: Apostra implementation/testing. |
## Targeting
| Feature | Availability | References | Comments |
| - | - | - | - |
| Geo Countries | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Available through Apostra. |
| Geo Regions | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Available through Apostra. |
| Geo Postal Areas | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Available through Apostra. |
| Language | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | targeting.demographics.languages with provider inventory preflight |
| Audience Include | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | targeting.segments INCLUDE |
| Audience Exclude | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | targeting.segments EXCLUDE |
| Signals | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Governed interest and segment IDs |
| Frequency Cap | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Available through Apostra. |
## Reporting
Apostra reports Snap Ads delivery day by day (there is no hourly delivery breakdown), aggregated nightly from adapter-reported delivery, in the advertiser's home currency using the booked or historical exchange rate — any cross-currency rollup is always labeled an estimate and is never used for billing. See [Reporting overview](/v2/guides/reporting-overview) for the full model. Apostra does not currently publish an attribution-finality window for Snap Ads; the table below lists every metric the adapter surfaces today.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Impressions | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Values must agree when both are returned |
| Clicks | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Mapped to canonical clicks |
| Spend | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Provider micro-currency converted exactly |
| Completed Views | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Completed video views |
| Reach | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Unique logged-in Snapchat accounts |
| Conversions | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Purchase conversions |
| Conversion Value | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Provider microcurrency converted to account-currency major units |
| Leads | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Native lead submissions |
| Screen Time | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Projected to average video view time |
| Granularity | 🟢 Available | [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview) | Campaign and ad-squad timeseries |
## Webhooks and async lifecycle
Snap Ads does not push status, delivery, or reporting events into Apostra — every campaign product declares `supports_webhooks: false`.
* **Status.** Apostra does not currently poll Snap Ads in the background for creative or media-buy status changes; status reflects what the adapter returned on your last call.
* **Delivery.** Apostra fetches delivery numbers (spend, impressions, clicks) live from Snap Ads on demand, only when you call `get_media_buy_delivery` directly. The `/reporting/metrics` endpoints never call the adapter either way — they read from a separately-populated store (see Reporting below), so a reporting request does not itself refresh provider delivery.
This is separate from Apostra's own buyer webhook subscriptions — an unrelated account-level event feed (currently `discovery.revision` only), documented in [Buyer webhooks](/v2/buyer/webhooks).
## Market and account availability
Apostra does not promise that every feature is enabled for every account or
market. Availability is checked against your selected ad account rather than a global market list on this page.
## Important limitations
* **Apostra implementation/testing.** Snap preview requires an image or video creative already synchronized to the selected ad account; unsynchronized creatives and explicit placement selections cannot be previewed.
* **Apostra implementation/testing.** Catalog traffic, app installs, Story Ads, Collection Ads, lead generation, Sponsored Snaps, and Commercials are not available through Apostra.
* **both Apostra's implementation and the ad platform's API or account eligibility.** Promoting an existing Public Profile Snap is still being tested, and Snap must allow the selected profile to use it.
* **Apostra implementation/testing.** Governed CRM audience activation remains protected until the dedicated consented fixture is certified.
* **Apostra implementation/testing.** Apostra uses Snap's automatic placement choice instead of exposing individual placements.
* **the ad platform's API or your account's eligibility.** Account permissions and Snap feature access can narrow audience, Pixel, catalog, and creative availability.
* **Apostra implementation/testing.** Apostra has not yet completed end-to-end account validation for the full Snap campaign lifecycle.
* **the ad platform's API or your account's eligibility.** Web Conversions (and any other Pixel-optimized product) requires the selected Snap Pixel to have enough tracked conversion events. The conversion window is fixed by the product/optimization goal you choose, not something you select separately. When Apostra can complete its preflight check, a pixel below Snap's eligibility threshold is refused up front with SNAP\_PIXEL\_INELIGIBLE. If that check itself fails (a transient Snap-side error), Apostra proceeds and Snap's own validation is the backstop, so an ineligible pixel can occasionally still surface as a generic rejection after resources are created and rolled back.
## Technical mapping
Engineering-level detail — raw provider objectives, billing enums, optimization
codes, and object-field mappings. Not required reading to understand what
Apostra can do with Snap Ads; see the tables above for that.
| Product | Provider objective | Optimization | Billing |
| - | - | - | - |
| Awareness (`snap_awareness`) | `AWARENESS_AND_ENGAGEMENT` | `IMPRESSIONS` | `IMPRESSION` |
| Traffic (`snap_traffic`) | `TRAFFIC` | `SWIPES` | `IMPRESSION` |
| Web Conversions (`snap_web_conversions`) | `SALES` | `PIXEL_PURCHASE` | `IMPRESSION` |
| Catalog Traffic (`snap_catalog_traffic`) | `TRAFFIC` | `SWIPES` | `IMPRESSION` |
| App Installs (`snap_app_installs`) | `APP_PROMOTION` | `APP_INSTALLS` | `IMPRESSION` |
| Story Ads (`snap_story_ads`) | `AWARENESS_AND_ENGAGEMENT` | `STORY_OPENS` | `IMPRESSION` |
| Collection (`snap_collection`) | `SALES` | `SWIPES` | `IMPRESSION` |
| Lead Gen (`snap_lead_gen`) | `LEADS` | `LEAD_FORM_SUBMISSIONS` | `IMPRESSION` |
| Sponsored Snaps (`snap_sponsored_snaps`) | `AWARENESS_AND_ENGAGEMENT` | `IMPRESSIONS` | `IMPRESSION` |
| Commercial (`snap_commercial`) | `AWARENESS_AND_ENGAGEMENT` | `VIDEO_VIEWS` | `IMPRESSION` |
| Format | Snap Ads shape |
| - | - |
| Ad Video 9x16 (`snap_ad_video_9x16`) | SNAP\_AD creative backed by video media |
| Ad Image 9x16 (`snap_ad_image_9x16`) | SNAP\_AD creative backed by image media |
| Existing Post (`snap_existing_post`) | authorized Public Profile Snap |
| Story Ad (`story_ad`) | COMPOSITE |
| Collection Ad (`collection_ad`) | COLLECTION |
| Lead Gen Ad (`lead_gen_ad`) | LEAD\_GENERATION |
| Promoted Offerings (`snap_promoted_offerings`) | one ad squad and creative per offering |
| Generated Image 9x16 (`snap_generated_image_9x16`) | generated vertical image |
| Generated Offerings (`snap_generated_offerings`) | generated image per offering |
| Sponsored Snap (`snap_sponsored_snap`) | WEB\_VIEW creative with chat\_properties |
| Generated Sponsored Snap (`snap_generated_sponsored_snap`) | generated WEB\_VIEW creative with chat\_properties |
| Commercial (`snap_commercial`) | SNAP\_AD with SIX\_SECONDS forced view |
| Targeting choice | Provider field |
| - | - |
| Geo Countries (`geo_countries`) | `targeting.geos.country_code` |
| Geo Regions (`geo_regions`) | `ISO subdivision → provider region_id with provenance` |
| Geo Postal Areas (`geo_postal_areas`) | `targeting.geos.postal_code` |
| Language (`language`) | `targeting.demographics.languages with provider inventory preflight` |
| Audience Include (`audience_include`) | `targeting.segments INCLUDE` |
| Audience Exclude (`audience_exclude`) | `targeting.segments EXCLUDE` |
| Signals (`signals`) | `Governed interest and segment IDs` |
| Frequency Cap (`frequency_cap`) | `cap_and_exclusion_config` |
| Apostra output | Snap Ads source |
| - | - |
| Impressions (`impressions`) | `impressions \| paid_impressions` |
| Clicks (`clicks`) | `swipes` |
| Spend (`spend`) | `spend` |
| Completed Views (`completed_views`) | `view_completion` |
| Reach (`reach`) | `uniques` |
| Conversions (`conversions`) | `conversion_purchases` |
| Conversion Value (`conversion_value`) | `conversion_purchases_value` |
| Leads (`leads`) | `native_leads` |
| Screen Time (`screen_time`) | `screen_time_millis` |
| Granularity (`granularity`) | `TOTAL \| DAY \| HOUR` |
## Provider documentation
* [Snap Marketing API overview](https://developers.snap.com/marketing-api/Ads-API/overview)
* [Snap Campaigns](https://developers.snap.com/marketing-api/Ads-API/campaigns)
* [Snap Ad Squads](https://developers.snap.com/marketing-api/Ads-API/ad-squads)
* [Snap Measurement](https://developers.snap.com/marketing-api/Ads-API/measurement)
* [Snap Creatives](https://developers.snap.com/marketing-api/Ads-API/creatives)
* [Snap Media](https://developers.snap.com/marketing-api/Ads-API/media)
* [Snap Lens and Filter buying](https://developers.snap.com/marketing-api/Ads-API/lenses)
* [Snap Dynamic Product Ads](https://developers.snap.com/marketing-api/Ads-API/dynamic-product-ads)
* [Snap Authentication](https://developers.snap.com/marketing-api/Ads-API/authentication)
# Spotify Ads compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/spotify
What you can and cannot do with Spotify Ads through Apostra.
This page compares the Spotify Ads capabilities represented in the
Apostra adapter with what Apostra currently makes available to buyers.
It answers a practical question: **what can I do through Apostra today, and
what would still require another path?**
**Last reviewed 2026-08-18.** This is Apostra's description of
its own adapter, based on the implementation and the linked provider
documentation. Spotify Ads has not reviewed or endorsed this page.
Availability can still vary by account permissions, market, and provider access.
## At a glance
| Area | 🟢 Available | 🟡 Partial | 🔵 Testing | ⚪ Planned |
| - | - | - | - | - |
| Campaign products | **0** | **0** | **4** | **3** |
| Creative formats | **0** | **0** | **0** | **8** |
| Adapter capabilities | **0** | **2** | **0** | **14** |
### How to read availability
* 🟢 **Available** — Offered through Apostra now, subject to your ad account's permissions and eligibility with the provider.
* 🟡 **Partial** — Some of the workflow is available; the Comments column explains the boundary.
* 🔵 **Testing** — Apostra is validating the workflow before offering it broadly.
* ⚪ **Planned** — Not offered through Apostra yet. The provider may still support it directly; Comments explains why and links a roadmap reference.
## Roadmap
The adapter roadmap is tracked in AI-1944 (a plain-text reference — Apostra's internal issue tracker is not public).
This roadmap tracks Spotify product completeness, provider limits, and live readiness. Every row that is not currently available shows
that reference or a more specific one in its References column, so customers
and partners can quote a stable identifier when asking about a capability.
## Connection, accounts, and identity
Spotify Ads connects to Apostra through an OAuth grant. Connecting a Business account gives Apostra access to each Ad account it can reach — you then map each one to your advertisers. The mapped account decides which Spotify Ads tenant a buy runs on: a media buy for an unmapped advertiser fails closed with `account_mapping_required` rather than silently using another account. Connection, account-discovery, and revocation behavior is the same across ad platforms — see [Platform accounts and advertiser mapping](/v2/concepts/ad-platform-accounts) for the full model and the [Spotify Ads section of the connecting guide](/v2/guides/connecting-ad-platforms#spotify) for Spotify Ads-specific permissions.
| Apostra concept | Spotify Ads concept | What it represents |
| - | - | - |
| Account (`account`) | provider advertising account | The authenticated selected account is the execution boundary. |
| Media Buy (`media_buy`) | campaign | Media Buy maps to campaign. |
| Package (`package`) | ad set | Budget, targeting, optimization, and flight bindings. |
| Creative Assignment (`creative_assignment`) | creative | Creative materialization or assignment surface declared by current code. |
## Campaign products
| Feature | Availability | References | Comments |
| - | - | - | - |
| Audio Awareness | 🔵 Testing | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Video Takeover | 🔵 Testing | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Display Awareness | 🔵 Testing | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Podcast Promotion | 🔵 Testing | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Podcast Audio | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra has not completed the publisher-availability checks required for this product. Current boundary: Apostra implementation/testing. |
| Podcast Crosspromo | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra has not completed the publisher-availability checks required for this product. Current boundary: Apostra implementation/testing. |
| Traffic | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra has not completed the publisher-availability checks required for this product. Current boundary: Apostra implementation/testing. |
## Creative formats
These rows describe **Apostra availability**, not a claim that the ad platform
itself prohibits an unavailable format.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Audio Standard | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra represents audio with a companion image, but audio audition, creative submission, and faithful readback have not completed the end-to-end buyer workflow. Current boundary: Apostra implementation/testing. |
| Video Takeover | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| In Feed Video | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| In Feed Display | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Display Leaderboard | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Podcast Audio | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Companion | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Audio | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
## What the adapter can do
| Feature | Availability | References | Comments |
| - | - | - | - |
| List Accounts | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Accounts | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Account synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Products | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Creatives | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| List Creatives | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Preview Creative | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Spotify can return a preview link for a saved ad, but not for an ad draft, and it documents neither the link's expiry nor anonymous access. Apostra must prove selected-account identity, URL safety, access, and lifetime before returning it. Current boundary: Apostra implementation/testing. |
| Create Media Buy | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Media Buys | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Update Media Buy | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Media Buy Delivery | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Signals | 🟡 Partial | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Existing Spotify audiences are visible as read-only inventory, but Apostra does not currently make them selectable for campaign targeting. Current boundary: Apostra implementation/testing. |
| Sync Audiences | 🟡 Partial | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra can list and delete existing Spotify custom audiences. Lookalike audiences are read-only; creating audiences, changing their members, and attaching any audience to campaigns are unavailable. Current boundary: both Apostra's implementation and the ad platform's API or account eligibility. |
| Sync Event Sources | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Log Event | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Event ingestion is not implemented. Current boundary: Apostra implementation/testing. |
| Sync Catalogs | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Catalog synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Account Financials | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Account financial reporting is not implemented. Current boundary: Apostra implementation/testing. |
## Placement choices
Apostra does not expose a separate Spotify Ads placement selector.
## Targeting
| Feature | Availability | References | Comments |
| - | - | - | - |
| Geo Countries | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Geo Regions | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Audience Include | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Spotify audience inclusion is not available through Apostra. Discovered audiences remain read-only inventory. Current boundary: Apostra implementation/testing. |
| Audience Exclude | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Spotify audience exclusion is not available through Apostra. Discovered audiences remain read-only inventory. Current boundary: Apostra implementation/testing. |
## Reporting
Apostra reports Spotify Ads delivery day by day (there is no hourly delivery breakdown), aggregated nightly from adapter-reported delivery, in the advertiser's home currency using the booked or historical exchange rate — any cross-currency rollup is always labeled an estimate and is never used for billing. See [Reporting overview](/v2/guides/reporting-overview) for the full model. Apostra does not currently publish an attribution-finality window for Spotify Ads; the table below lists every metric the adapter surfaces today.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Impressions | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Campaign and package totals |
| Spend | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Account-currency amount |
| Clicks | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Campaign and package totals |
| Completed Views | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Completed plays; streams and starts are not relabelled as views |
| Reach | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Cumulative unique listeners for the report window |
| Frequency | ⚪ Planned | [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/) · AI-1944 | Provider total for the same cumulative window |
## Webhooks and async lifecycle
Spotify Ads does not push status, delivery, or reporting events into Apostra — every campaign product declares `supports_webhooks: false`.
* **Status.** Apostra does not currently poll Spotify Ads in the background for creative or media-buy status changes; status reflects what the adapter returned on your last call.
* **Delivery.** Apostra has not yet made on-demand delivery fetching generally available for Spotify Ads — see Get Media Buy Delivery in "What the adapter can do" above. The `/reporting/metrics` endpoints never call the adapter either way — they read from a separately-populated store (see Reporting below), so a reporting request does not itself refresh provider delivery.
This is separate from Apostra's own buyer webhook subscriptions — an unrelated account-level event feed (currently `discovery.revision` only), documented in [Buyer webhooks](/v2/buyer/webhooks).
## Market and account availability
Apostra does not promise that every feature is enabled for every account or
market. Availability is checked against your selected ad account rather than a global market list on this page.
## Important limitations
* **Apostra implementation/testing.** This page is not yet a confirmed complete catalog of every Spotify Ads creation path.
* **Apostra implementation/testing.** The listed campaign products exist in the current adapter code but are still being certified before general availability.
* **the ad platform's API or your account's eligibility.** Brief planning interprets objective, age, geography, interests, format, and podcast context. A recommendation does not guarantee that the selected Spotify account can execute it.
* **the ad platform's API.** The Spotify Ads API does not expose terminal campaign cancellation through this integration. Pause is reversible, and Apostra never reports it as canceled.
* **Apostra implementation/testing.** Existing Spotify custom audiences can be listed and deleted. Lookalike audiences are read-only, and no audience can be created, changed, included, or excluded from campaign targeting through Apostra.
* **both Apostra's implementation and the ad platform's API or account eligibility.** Product selection fixes Spotify-owned in-stream or in-feed delivery; there is no separate placement selector. Placement reporting is returned only after provider readback confirms music-only inventory and no off-Spotify impressions.
* **Apostra implementation/testing.** Spotify audio formats are listed for planning, but Apostra does not yet offer audio audition or a complete creative preview, submission, and readback workflow.
## Technical mapping
Engineering-level detail — raw provider objectives, billing enums, optimization
codes, and object-field mappings. Not required reading to understand what
Apostra can do with Spotify Ads; see the tables above for that.
| Product | Provider objective | Optimization | Billing |
| - | - | - | - |
| Audio Awareness (`spotify_audio_awareness`) | `AWARENESS` | `REACH` | `not_exposed_in_public_config` |
| Video Takeover (`spotify_video_takeover`) | `AWARENESS` | `VIDEO_VIEWS` | `not_exposed_in_public_config` |
| Display Awareness (`spotify_display_awareness`) | `AWARENESS` | `IMPRESSIONS` | `not_exposed_in_public_config` |
| Podcast Promotion (`spotify_podcast_promotion`) | `ENGAGEMENT_ON_SPOTIFY` | `STREAMS` | `not_exposed_in_public_config` |
| Podcast Audio (`spotify_podcast_audio`) | `AWARENESS` | `IMPRESSIONS` | `not_exposed_in_public_config` |
| Podcast Crosspromo (`spotify_podcast_crosspromo`) | `ENGAGEMENT_ON_SPOTIFY` | `STREAMS` | `not_exposed_in_public_config` |
| Traffic (`spotify_traffic`) | `WEBSITE_TRAFFIC` | `CLICKS` | `not_exposed_in_public_config` |
| Format | Spotify Ads shape |
| - | - |
| Audio Standard (`audio_standard`) | Audio asset with a companion image. |
| Video Takeover (`video_takeover`) | Canonical Code Format |
| In Feed Video (`in_feed_video`) | Canonical Code Format |
| In Feed Display (`in_feed_display`) | Canonical Code Format |
| Display Leaderboard (`display_leaderboard`) | Canonical Code Format |
| Podcast Audio (`podcast_audio`) | Canonical Code Format |
| Generated Companion (`spotify_generated_companion`) | Canonical Code Format |
| Generated Audio (`spotify_generated_audio`) | Canonical Code Format |
| Targeting choice | Provider field |
| - | - |
| Geo Countries (`geo_countries`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Geo Regions (`geo_regions`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Audience Include (`audience_include`) | `Rejected before provider I/O until the campaign writer can attach provider audience IDs.` |
| Audience Exclude (`audience_exclude`) | `Rejected before provider I/O until the campaign writer can attach provider audience IDs.` |
| Apostra output | Spotify Ads source |
| - | - |
| Impressions (`impressions`) | `IMPRESSIONS` |
| Spend (`spend`) | `SPEND` |
| Clicks (`clicks`) | `CLICKS` |
| Completed Views (`completed_views`) | `COMPLETES` |
| Reach (`reach`) | `REACH` |
| Frequency (`frequency`) | `FREQUENCY` |
## Provider documentation
* [Spotify Ads public documentation](https://ads.spotify.com/en-US/help-center/)
* [Spotify saved-ad preview reference](https://developer.spotify.com/documentation/ads-api/reference/v3.0/getAd)
* [Spotify saved-ad list reference](https://developer.spotify.com/documentation/ads-api/reference/v3.0/getAds)
* [Spotify saved-ad creation reference](https://developer.spotify.com/documentation/ads-api/reference/v3.0/createAd)
* [Spotify ad-draft reference](https://developer.spotify.com/documentation/ads-api/reference/v3.0/createAdDraft)
# TikTok Ads compatibility
Source: https://docs.apostra.com/v2/guides/ad-platform-compatibility/tiktok
What you can and cannot do with TikTok Ads through Apostra.
This page compares the TikTok Ads capabilities represented in the
Apostra adapter with what Apostra currently makes available to buyers.
It answers a practical question: **what can I do through Apostra today, and
what would still require another path?**
**Last reviewed 2026-08-27.** This is Apostra's description of
its own adapter, based on the implementation and the linked provider
documentation. TikTok Ads has not reviewed or endorsed this page.
Availability can still vary by account permissions, market, and provider access.
## At a glance
| Area | 🟢 Available | 🟡 Partial | 🔵 Testing | ⚪ Planned |
| - | - | - | - | - |
| Campaign products | **0** | **0** | **43** | **4** |
| Creative formats | **0** | **0** | **0** | **8** |
| Adapter capabilities | **1** | **0** | **0** | **15** |
### How to read availability
* 🟢 **Available** — Offered through Apostra now, subject to your ad account's permissions and eligibility with the provider.
* 🟡 **Partial** — Some of the workflow is available; the Comments column explains the boundary.
* 🔵 **Testing** — Apostra is validating the workflow before offering it broadly.
* ⚪ **Planned** — Not offered through Apostra yet. The provider may still support it directly; Comments explains why and links a roadmap reference.
## Roadmap
The adapter roadmap is tracked in AI-1942 (a plain-text reference — Apostra's internal issue tracker is not public).
This roadmap tracks TikTok product completeness, account resources, and live readiness. Every row that is not currently available shows
that reference or a more specific one in its References column, so customers
and partners can quote a stable identifier when asking about a capability.
## Connection, accounts, and identity
TikTok Ads connects to Apostra through an OAuth grant. Connecting a Business Center account gives Apostra access to each Advertiser account (plus channel identities for Spark Ads) it can reach — you then map each one to your advertisers. The mapped account decides which TikTok Ads tenant a buy runs on: a media buy for an unmapped advertiser fails closed with `account_mapping_required` rather than silently using another account. Connection, account-discovery, and revocation behavior is the same across ad platforms — see [Platform accounts and advertiser mapping](/v2/concepts/ad-platform-accounts) for the full model and the [TikTok Ads section of the connecting guide](/v2/guides/connecting-ad-platforms#tiktok) for TikTok Ads-specific permissions.
| Apostra concept | TikTok Ads concept | What it represents |
| - | - | - |
| Account (`account`) | provider advertising account | The authenticated selected account is the execution boundary. |
| Media Buy (`media_buy`) | campaign | Media Buy maps to campaign. |
| Package (`package`) | ad group | Budget, targeting, optimization, and flight bindings. |
| Creative Assignment (`creative_assignment`) | ad or identity-backed creative | Creative materialization or assignment surface declared by current code. |
## Campaign products
| Feature | Availability | References | Comments |
| - | - | - | - |
| Reach | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Video Views | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Video Views 15s | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Traffic | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart Traffic Clicks | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart Traffic Landing Page Views | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Followers | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Profile Visits | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Conversions | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Sales Website Catalog | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Installs | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart App Install | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart App In App Event | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart App Value | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart App Value Purchase Target Roas Day7 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart App Value Purchase Target Roas Day0 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart App Value Ad Revenue Highest Value Day7 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart App Value Ad Revenue Highest Value Day0 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart App Value Ad Revenue Target Roas Day7 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart App Value Ad Revenue Target Roas Day0 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Install Clicks | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Install In App Event | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Retargeting In App Event | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Retargeting Value Maximum Delivery | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Install Value | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Install Value Target Roas Day7 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Install Value Purchase Target Roas Day0 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Install Value Ad Revenue Highest Value Day7 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Install Value Ad Revenue Highest Value Day0 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Install Value Ad Revenue Target Roas Day7 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| App Install Value Ad Revenue Target Roas Day0 | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Lead Gen | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Lead Gen Website | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Lead Gen Direct Message Clicks | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Lead Gen Direct Message Conversations | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Lead Gen Phone Call Clicks | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Lead Gen Messaging App Clicks | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Lead Gen Messaging App Conversations | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Smart Web Conversion | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra cannot yet verify that the selected account's Pixel supports the required event. Current boundary: Apostra implementation/testing. |
| Smart Web Value | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra cannot yet verify that the selected account's Pixel supports value optimization. Current boundary: Apostra implementation/testing. |
| Smart Catalog Conversion | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra cannot yet verify that the selected account's Pixel supports the required event. Current boundary: Apostra implementation/testing. |
| Smart Catalog Value | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra cannot yet verify that the selected account's Pixel supports value optimization. Current boundary: Apostra implementation/testing. |
| Search Traffic Clicks Maximum Delivery | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Search Traffic Clicks Cost Cap | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Search Traffic Landing Page Views Maximum Delivery | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Search Traffic Landing Page Views Cost Cap | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
| Search Web Conversions Target Cpa | 🔵 Testing | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra has implemented this product in code but has not yet completed testing for general availability. Current boundary: Apostra implementation/testing. |
## Optional maximum-CPC bidding
Traffic (`tiktok_traffic`) and App Install Clicks (`tiktok_app_install_clicks`) use automatic bidding when a package omits `bid_price`.
To set a cap for either product, supply a positive `packages[].bid_price`. Apostra then uses TikTok custom bidding and treats the value as the maximum cost per click, as indicated by `max_bid: true` on the product's pricing option.
Apostra validates a supplied bid, including the advertiser currency's minor-unit precision, before creating or updating a TikTok campaign. After each write, it reads the ad group back and reports success only when the bid type and maximum CPC match the request. Before supplying a maximum CPC, rediscover either product with `get_products`; a selection saved from an earlier discovery response does not advertise this option. An existing automatic-bidding package can move to maximum-CPC bidding only when it was created through Apostra's manual TikTok flow from a response advertising this option and its durable product and bidding provenance are available; missing or mismatched provenance fails without a provider write. Later maximum-CPC changes are supported. Clearing `bid_price` does not move a custom-bidding package back to automatic bidding. Other TikTok products retain their creation-time bidding modes; on update, Apostra rejects `bid_price` for an automatic-bidding package whose selected product did not advertise maximum-CPC bidding, and automatic-bidding package readback omits stale provider bid values rather than presenting them as an active buyer cap.
The custom-bid path has automated contract and readback coverage, but it has not yet been validated with a live TikTok account; account permissions and eligibility remain authoritative.
## Pricing notes
`pricing_options` omits `floor_price` and `price_guidance` for every TikTok product, including the two products above that support a maximum-CPC bid. TikTok exposes no discovery-time bid estimate, so Interchange no longer guesses a floor. Each product is still priced (`pricing_model`, `currency`); there is simply no platform-backed number to bid against yet.
## Creative formats
These rows describe **Apostra availability**, not a claim that the ad platform
itself prohibits an unavailable format.
| Feature | Availability | References | Comments |
| - | - | - | - |
| In Feed Video | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Image Ad | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Carousel | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Spark Video | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Promoted Offerings | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Image 9x16 | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Video 9x16 | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
| Generated Offerings | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra represents this creative format in its catalog, but has not completed the end-to-end buyer workflow and faithful readback needed to offer it. Current boundary: Apostra implementation/testing. |
## What the adapter can do
| Feature | Availability | References | Comments |
| - | - | - | - |
| List Accounts | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Accounts | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Account synchronization is not implemented. Current boundary: Apostra implementation/testing. |
| Get Products | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Creatives | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| List Creatives | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Preview Creative | 🟢 Available | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) | Available through Apostra. |
| Create Media Buy | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Media Buys | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Update Media Buy | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Media Buy Delivery | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Signals | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Audiences | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Event Sources | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Log Event | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Sync Catalogs | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Get Account Financials | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this capability; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
## Placement choices
Apostra does not expose a separate TikTok Ads placement selector.
## Targeting
| Feature | Availability | References | Comments |
| - | - | - | - |
| Audience Exclude | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Audience Include | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Geo Countries | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Geo Regions | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Frequency Cap | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Signals | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
| Keyword Targets | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Apostra does not currently offer this targeting choice; the linked roadmap reference tracks the missing workflow and disposition. Current boundary: Apostra implementation/testing. |
## Reporting
Apostra reports TikTok Ads delivery day by day (there is no hourly delivery breakdown), aggregated nightly from adapter-reported delivery, in the advertiser's home currency using the booked or historical exchange rate — any cross-currency rollup is always labeled an estimate and is never used for billing. See [Reporting overview](/v2/guides/reporting-overview) for the full model. Apostra does not currently publish an attribution-finality window for TikTok Ads; the table below lists every metric the adapter surfaces today.
| Feature | Availability | References | Comments |
| - | - | - | - |
| Impressions | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Campaign and ad-group totals |
| Spend | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Account-currency amount |
| Clicks | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Destination clicks |
| Completed Views | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | 100% video completions |
| Conversions | ⚪ Planned | [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs) · AI-1942 | Provider conversion count |
## Webhooks and async lifecycle
TikTok Ads does not push status, delivery, or reporting events into Apostra — every campaign product declares `supports_webhooks: false`.
* **Status.** Apostra does not currently poll TikTok Ads in the background for creative or media-buy status changes; status reflects what the adapter returned on your last call.
* **Delivery.** Apostra has not yet made on-demand delivery fetching generally available for TikTok Ads — see Get Media Buy Delivery in "What the adapter can do" above. The `/reporting/metrics` endpoints never call the adapter either way — they read from a separately-populated store (see Reporting below), so a reporting request does not itself refresh provider delivery.
This is separate from Apostra's own buyer webhook subscriptions — an unrelated account-level event feed (currently `discovery.revision` only), documented in [Buyer webhooks](/v2/buyer/webhooks).
## Market and account availability
Apostra does not promise that every feature is enabled for every account or
market. Availability is checked against your selected ad account rather than a global market list on this page.
## Important limitations
* **Apostra implementation/testing.** Apostra reviewed 58 TikTok Ads creation flows: 38 exist in the current adapter code, 19 are blocked, and one is unavailable.
* **Apostra implementation/testing.** The implemented campaign products are still being certified before general availability.
* **Apostra implementation/testing.** Briefs with unresolved exclusions or interests, unverified creative localization, or instruction-like prompt text return no runnable products while Apostra asks for clarification.
* **Apostra implementation/testing.** TikTok preview is limited to saved in-feed videos. Other creative families and unsynced or transient specifications are rejected rather than approximated.
* **the ad platform's API or your account's eligibility.** TikTok account permissions, Pixels, apps, catalogs, identities, and market eligibility can further narrow availability.
* **the ad platform's API or your account's eligibility.** Smart+ Website and Smart+ Website Catalog conversion and value products stay unavailable until the selected account's event statistics prove the required Purchase conversion or value eligibility.
* **the ad platform's API.** For some campaign objectives, TikTok's platform automatically extends delivery to TikTok's search results page in addition to your selected placement. TikTok does not offer a way to opt out of this for these objectives, and Apostra cannot disable it on your behalf.
## Technical mapping
Engineering-level detail — raw provider objectives, billing enums, optimization
codes, and object-field mappings. Not required reading to understand what
Apostra can do with TikTok Ads; see the tables above for that.
| Product | Provider objective | Optimization | Billing |
| - | - | - | - |
| Reach (`tiktok_reach`) | `REACH` | `REACH` | `CPM` |
| Video Views (`tiktok_video_views`) | `VIDEO_VIEWS` | `ENGAGED_VIEW` | `CPV` |
| Video Views 15s (`tiktok_video_views_15s`) | `VIDEO_VIEWS` | `ENGAGED_VIEW_FIFTEEN` | `CPV` |
| Traffic (`tiktok_traffic`) | `TRAFFIC` | `CLICK` | `CPC` |
| Smart Traffic Clicks (`tiktok_smart_traffic_clicks`) | `TRAFFIC` | `CLICK` | `CPC` |
| Smart Traffic Landing Page Views (`tiktok_smart_traffic_landing_page_views`) | `TRAFFIC` | `LANDING_PAGE_VIEW` | `OCPM` |
| Followers (`tiktok_followers`) | `ENGAGEMENT` | `FOLLOWERS` | `OCPM` |
| Profile Visits (`tiktok_profile_visits`) | `ENGAGEMENT` | `PAGE_VISIT` | `CPC` |
| Conversions (`tiktok_conversions`) | `WEB_CONVERSIONS` | `CONVERSION` | `OCPM` |
| Sales Website Catalog (`tiktok_sales_website_catalog`) | `PRODUCT_SALES` | `CONVERT` | `OCPM` |
| App Installs (`tiktok_app_installs`) | `APP_PROMOTION` | `INSTALL` | `OCPM` |
| Smart App Install (`tiktok_smart_app_install`) | `APP_PROMOTION` | `INSTALL` | `OCPM` |
| Smart App In App Event (`tiktok_smart_app_in_app_event`) | `APP_PROMOTION` | `IN_APP_EVENT` | `OCPM` |
| Smart App Value (`tiktok_smart_app_value`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| Smart App Value Purchase Target Roas Day7 (`tiktok_smart_app_value_purchase_target_roas_day7`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| Smart App Value Purchase Target Roas Day0 (`tiktok_smart_app_value_purchase_target_roas_day0`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| Smart App Value Ad Revenue Highest Value Day7 (`tiktok_smart_app_value_ad_revenue_highest_value_day7`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| Smart App Value Ad Revenue Highest Value Day0 (`tiktok_smart_app_value_ad_revenue_highest_value_day0`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| Smart App Value Ad Revenue Target Roas Day7 (`tiktok_smart_app_value_ad_revenue_target_roas_day7`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| Smart App Value Ad Revenue Target Roas Day0 (`tiktok_smart_app_value_ad_revenue_target_roas_day0`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| App Install Clicks (`tiktok_app_install_clicks`) | `APP_PROMOTION` | `CLICK` | `CPC` |
| App Install In App Event (`tiktok_app_install_in_app_event`) | `APP_PROMOTION` | `IN_APP_EVENT` | `OCPM` |
| App Retargeting In App Event (`tiktok_app_retargeting_in_app_event`) | `APP_PROMOTION` | `IN_APP_EVENT` | `OCPM` |
| App Retargeting Value Maximum Delivery (`tiktok_app_retargeting_value_maximum_delivery`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| App Install Value (`tiktok_app_install_value`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| App Install Value Target Roas Day7 (`tiktok_app_install_value_target_roas_day7`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| App Install Value Purchase Target Roas Day0 (`tiktok_app_install_value_purchase_target_roas_day0`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| App Install Value Ad Revenue Highest Value Day7 (`tiktok_app_install_value_ad_revenue_highest_value_day7`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| App Install Value Ad Revenue Highest Value Day0 (`tiktok_app_install_value_ad_revenue_highest_value_day0`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| App Install Value Ad Revenue Target Roas Day7 (`tiktok_app_install_value_ad_revenue_target_roas_day7`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| App Install Value Ad Revenue Target Roas Day0 (`tiktok_app_install_value_ad_revenue_target_roas_day0`) | `APP_PROMOTION` | `VALUE` | `OCPM` |
| Lead Gen (`tiktok_lead_gen`) | `LEAD_GENERATION` | `LEAD_GENERATION` | `OCPM` |
| Lead Gen Website (`tiktok_lead_gen_website`) | `LEAD_GENERATION` | `CONVERSION` | `OCPM` |
| Lead Gen Direct Message Clicks (`tiktok_lead_gen_direct_message_clicks`) | `LEAD_GENERATION` | `CLICK` | `CPC` |
| Lead Gen Direct Message Conversations (`tiktok_lead_gen_direct_message_conversations`) | `LEAD_GENERATION` | `CONVERSATION` | `OCPM` |
| Lead Gen Phone Call Clicks (`tiktok_lead_gen_phone_call_clicks`) | `LEAD_GENERATION` | `CLICK` | `CPC` |
| Lead Gen Messaging App Clicks (`tiktok_lead_gen_messaging_app_clicks`) | `LEAD_GENERATION` | `CLICK` | `CPC` |
| Lead Gen Messaging App Conversations (`tiktok_lead_gen_messaging_app_conversations`) | `LEAD_GENERATION` | `CONVERSATION` | `OCPM` |
| Smart Web Conversion (`tiktok_smart_web_conversion`) | `WEB_CONVERSIONS` | `CONVERSION` | `OCPM` |
| Smart Web Value (`tiktok_smart_web_value`) | `WEB_CONVERSIONS` | `VALUE` | `OCPM` |
| Smart Catalog Conversion (`tiktok_smart_catalog_conversion`) | `WEB_CONVERSIONS` | `CONVERSION` | `OCPM` |
| Smart Catalog Value (`tiktok_smart_catalog_value`) | `WEB_CONVERSIONS` | `VALUE` | `OCPM` |
| Search Traffic Clicks Maximum Delivery (`tiktok_search_traffic_clicks_maximum_delivery`) | `TRAFFIC` | `CLICK` | `CPC` |
| Search Traffic Clicks Cost Cap (`tiktok_search_traffic_clicks_cost_cap`) | `TRAFFIC` | `CLICK` | `CPC` |
| Search Traffic Landing Page Views Maximum Delivery (`tiktok_search_traffic_landing_page_views_maximum_delivery`) | `TRAFFIC` | `LANDING_PAGE_VIEW` | `OCPM` |
| Search Traffic Landing Page Views Cost Cap (`tiktok_search_traffic_landing_page_views_cost_cap`) | `TRAFFIC` | `LANDING_PAGE_VIEW` | `OCPM` |
| Search Web Conversions Target Cpa (`tiktok_search_web_conversions_target_cpa`) | `WEB_CONVERSIONS` | `CONVERSION` | `OCPM` |
| Format | TikTok Ads shape |
| - | - |
| In Feed Video (`tiktok_in_feed_video`) | Canonical Code Format |
| Image Ad (`tiktok_image_ad`) | Canonical Code Format |
| Carousel (`tiktok_carousel`) | Canonical Code Format |
| Spark Video (`tiktok_spark_video`) | Canonical Code Format |
| Promoted Offerings (`tiktok_promoted_offerings`) | Canonical Code Format |
| Generated Image 9x16 (`tiktok_generated_image_9x16`) | Canonical Code Format |
| Generated Video 9x16 (`tiktok_generated_video_9x16`) | Canonical Code Format |
| Generated Offerings (`tiktok_generated_offerings`) | Canonical Code Format |
| Targeting choice | Provider field |
| - | - |
| Audience Exclude (`audience_exclude`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Audience Include (`audience_include`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Geo Countries (`geo_countries`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Geo Regions (`geo_regions`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Frequency Cap (`frequency_cap`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Signals (`signals`) | `Accepted by the current adapter targeting gate; provider eligibility remains account-bound.` |
| Keyword Targets (`keyword_targets`) | `Required activation input on tiktok_search_* products only; 1-1000 unique (keyword, match_type) entries validated and rejected before any provider mutation. Not accepted on non-Search products.` |
| Apostra output | TikTok Ads source |
| - | - |
| Impressions (`impressions`) | `impressions` |
| Spend (`spend`) | `spend` |
| Clicks (`clicks`) | `clicks` |
| Completed Views (`completed_views`) | `video_views_p100` |
| Conversions (`conversions`) | `conversion` |
## Provider documentation
* [TikTok Ads public documentation](https://business-api.tiktok.com/portal/docs)
* [TikTok preview endpoint](https://business-api.tiktok.com/portal/docs?id=1739403070695426)
* [TikTok advertiser video search endpoint](https://business-api.tiktok.com/portal/docs?id=1740050472224769)
* [TikTok call-to-action enumeration](https://business-api.tiktok.com/portal/docs?id=1737174886619138)
* [TikTok ad-create endpoint](https://business-api.tiktok.com/portal/docs?id=1739953377508354)
# Ad Platform Resource Capabilities
Source: https://docs.apostra.com/v2/guides/ad-platform-resource-capabilities
Which audiences, signals, and event sources each connected ad platform can report — and how complete each read is
When Apostra reads audiences, signals, or event sources from a connected ad
platform account (see [Connecting Ad Platform
Accounts](/v2/guides/connecting-ad-platforms)), what comes back depends on the
platform. Some platforms return the account's complete inventory. Others can
only see objects that were created through Apostra, or return a single derived
entry rather than a real inventory read. This page documents that coverage per
platform so you know when a result is the whole picture and when it is not.
This page covers direct ad platform adapter connections (Meta, TikTok,
Google Ads, and similar). On platform-composed storefronts, the same question
is answered by the `get_account_resources` tool, which labels capability and
coverage per source on every response — see
[AdCP on the buy side](/v2/concepts/adcp-on-the-buy-side#linked-account-scope).
Direct adapter storefronts do not expose that tool.
## Reading the tables
Each read falls into one of three coverage classes:
| Coverage | Meaning |
| - | - |
| **Full inventory** | A discovery read returns every eligible object in the account. Treat the result as the account's complete inventory. |
| **Synthesized** | The result is a single entry derived from the account itself (for example, a tag id computed from the account id), not a read of the platform's inventory. |
| **Seller managed** | The platform returns objects it manages natively — you did not create them and cannot modify or delete them through Apostra. |
Two more distinctions matter when interpreting any of these reads:
* **Discovery is not attachment.** These reads answer "what exists in the
account right now" by asking the platform directly. An object appearing in a
discovery read does not mean it is attached to, or usable by, any particular
campaign — and an object synced through Apostra remains tracked even if a
later discovery read cannot see it.
* **Only a full-inventory read is authoritative.** A synthesized or
seller-managed result is still real data, but absence from it proves nothing
about the account. Never treat a partial read as evidence that an object
does not exist.
## Audiences
Audience inventory is read with a discovery-mode `sync_audiences` call (no
audience list supplied, so nothing is created or modified). No audience
membership data is ever returned — identity, status, and match counts only.
| Platform | Coverage | Notes |
| - | - | - |
| Meta | Full inventory | All customer-list Custom Audiences in the ad account |
| Google Ads | Full inventory | |
| Reddit | Full inventory | |
| Spotify | Full inventory | |
| Pinterest | Full inventory | |
| LinkedIn | Full inventory | |
| TikTok | Full inventory | All Customer File audiences in the ad account |
| Snap | Full inventory | All Customer List audiences in the ad account |
| Amazon Ads | Not available | Amazon DSP audience reads require AMC integration, which is not yet supported |
### Audience write and deletion boundaries
Inventory coverage does not imply that every platform supports the same write
lifecycle. Apostra advertises only the operations that the official adapter can
confirm against the provider:
| Platform | CRM sync boundary |
| - | - |
| Meta, Snap | Create, add/remove members, retry to the same provider identity, check readiness, crash-safe exact-run recovery, and provider-confirmed deletion |
| TikTok | Customer File create/update/readiness/deletion uses durable tenant-owned identity and crash-safe exact-run recovery. Audience targeting is dark for ordinary buyers and is exercised only by the signed staging canary until fresh end-to-end evidence is signed. |
| Reddit | Create, add/remove members, readiness, targeting, and provider-confirmed deletion work in-process. Governed destructive canaries remain blocked because provider inventory exposes only the provider ID and a one-way buyer-identity marker, so crash cleanup cannot yet recover the exact buyer IDs. |
| Google Ads | Create, add/remove members, retry to the same tenant-owned Customer Match identity, check readiness, target Search and Display buys by canonical buyer audience ID, and perform provider-confirmed deletion. |
| LinkedIn | Existing eligible Matched Audiences remain discoverable as read-only full inventory. Apostra-managed creation, membership updates, targeting, and deletion are unavailable until LinkedIn approves the separate private Audiences application; this optional capability does not block campaign, measurement, professional-signal, or Lead Gen support. |
| Pinterest | A targetable Customer List Audience wraps a separate customer-list source. The staged adapter can create the pair, add/remove members, check both resources for readiness, retain durable tenant-owned identity, and archive the exact target audience. Pinterest v5 exposes no deletion operation for the backing customer-list source, so the governed lifecycle and audience targeting remain withheld pending cleanup policy and staged proof with a matchable CRM cohort. |
| Spotify | Complete custom-audience inventory and explicit deletion only; customer-list creation fails before provider mutation |
| Amazon Ads | Not available; Amazon DSP/AMC integration is required |
| AudioStack, Figma, Flashtalking | Audience sync is outside these creative and ad-serving integrations' scope |
The governed staging lifecycle exercises the full create,
target-in-a-paused-buy, update, and crash-safe delete path for Google Ads,
Meta, Snap, and Reddit. TikTok runs that same path only inside its signed,
bounded staging probe; it does not advertise ordinary audience targeting until
the resulting evidence is complete and signed.
Adapters with a boundary above remain fail-closed instead of inheriting that
parity claim.
## Signals
Signals are read with `get_signals`, a read-only projection of the platform's
audience inventory into AdCP signal references. Pinterest does not synthesize a
fallback signal: an empty provider inventory stays empty, and an account or
provider error fails the read.
| Platform | Availability |
| - | - |
| Meta, Google Ads, TikTok, Snap, Reddit, Pinterest, LinkedIn, Spotify | Available |
| Amazon Ads | Not available — requires AMC integration |
Signal coverage follows the platform's audience coverage above.
## Event sources
Event sources (pixels, tags, conversion actions) are read with a
discovery-mode `sync_event_sources` call. No event payloads are returned. Setup
code is included only where the platform supplies that exact account-scoped
artifact.
| Platform | Coverage | What comes back |
| - | - | - |
| Meta | Full inventory | All pixels in the account |
| TikTok | Full inventory | All pixels in the account. **TikTok Sales — Website Catalog Ads** readiness additionally proves that the selected Pixel is explicitly bound to the selected catalog. |
| Snap | Full inventory | All pixels in the account |
| Reddit | Full inventory | All pixels in the account |
| Spotify | Full inventory | One pixel per measurement dataset in the ad account |
| Google Ads | Seller managed | Conversion actions, all managed by the platform — visible but not created, modified, or deleted through Apostra |
| Amazon Ads | Seller managed | A single built-in entry for Amazon Attribution; Amazon has no external conversion API |
| Pinterest | Full inventory | Complete Conversion Tag inventory as returned by Pinterest for the selected ad account |
| LinkedIn | Full inventory | Every enabled `CONVERSIONS_API` conversion rule on the selected account, one AdCP event source per eligible rule |
Seller-managed entries describe measurement the platform performs natively.
Synthesized entries confirm the platform's tagging surface exists for the
account but say nothing about which tags are actually installed.
LinkedIn conversion rules are managed in LinkedIn Campaign Manager, not
through Apostra. `sync_event_sources` reads each enabled `CONVERSIONS_API` rule
the account already has and maps its LinkedIn conversion type to the closest
AdCP `event_type` (an unrecognized or newly introduced LinkedIn type maps to
`custom`, named after the conversion rule, rather than failing the read).
Insight Tag rules remain valid for native campaign optimization and
association, but are not returned by this direct-event sync and logging
surface. A disabled rule that is explicitly requested by `event_source_id`
fails with `EVENT_SOURCE_DISABLED` instead of being silently synced. Creating,
renaming, or deleting a conversion rule through `sync_event_sources` is not
supported and fails with
`CONVERSION_RULE_MUTATION_UNSUPPORTED` or `EVENT_SOURCE_UPDATE_UNSUPPORTED` —
manage the rule itself in Campaign Manager, then re-sync to pick up the
change. `log_event` accepts events only for a rule that is enabled and owned
by the selected account, and rejects an event whose type does not match the
rule's mapped `event_type`. A full-inventory `sync_event_sources` call (no
`event_sources` in the request) fails with
`LINKEDIN_EVENT_SOURCE_DISCOVERY_TOO_LARGE` for an account with more than 500
enabled `CONVERSIONS_API` rules, instead of silently returning only some of
them — request the specific `event_source_id` you need instead of full
discovery on an account that large.
Pinterest treats the ad account and Conversion Tag as separate identities.
Apostra rejects request-account mismatches before Pinterest I/O, validates every
returned Tag against the selected account, and relays executable setup only
when Pinterest returns that Tag's personalized `code_snippet`. A missing,
malformed, duplicate, or cross-account inventory fails closed. Server-side
Pinterest event delivery remains unavailable until the signed dedicated-account
validation is complete.
Existing Tags can be revalidated through `sync_event_sources`; changing or
removing Pinterest Tag configuration is not supported and fails visibly.
Successful discovery returns the complete inventory and is never truncated.
Pinterest responses are limited to 500 Tags and 256 KiB, and the complete
projected result is limited to 128 KiB. If any limit is exceeded, the operation
fails before replacing the last proven binding snapshot.
## Commerce catalogs and product sets
**Meta Sales — Website Catalog Sales** discovers every commerce catalog owned
by the selected account's Business Portfolio, then reads every product set in
each catalog. A catalog is offered only with non-empty product sets, and
each product-set candidate retains its parent catalog identity. These are
activation resources for `meta_sales_website_catalog`, not audience signals.
The read is fail-closed: missing Business Portfolio context, incomplete
pagination, invalid ownership data, or a product set returned under a different
catalog makes the resource inventory unavailable rather than producing a
partial candidate list. Creation repeats the provider read before mutation.
## Related
How account access is granted and what write access means
Organization vs advertiser accounts and how discovered accounts map to advertisers
# Exact Age Targeting
Source: https://docs.apostra.com/v2/guides/age-targeting
Request one exact age range in product discovery and preserve it through media-buy execution and readback
## Overview
Exact age targeting reaches a storefront in one of two ways, depending on the
kind of storefront.
**Storefronts on the Storefront-built or Agent-supplied path** (see
[Storefront agents in discovery](/v2/guides/discovery#storefront-agents-in-discovery)) take the exact range from the AdCP 3.2
`targeting_overlay.demographics.age` field on a `get_products` request:
```json theme={null}
{
"buying_mode": "brief",
"brief": "Build an awareness campaign in the United States.",
"targeting_overlay": {
"demographics": {
"age": { "min": 21, "max": 35, "include_unknown": false }
}
}
}
```
A storefront reads the structured field only from a request served at AdCP
3.2. When you set demographic targeting on a campaign, Apostra's proposal
requests send it to these storefronts as this field. Built-in platform adapters
and external sellers receive the brief only.
These storefronts apply hard age targeting **only** from the structured field.
The brief always reaches the storefront's sources, so a seller can use age
wording in it to recommend age-specific inventory or an age signal it sells;
age wording never causes a refusal. When the request carries no structured age,
a decision model reads the brief and decides whether it *requires or excludes*
an age range, says whether to reach people whose age is unknown, or only
*describes* the audience. To make that decision, the storefront sends the
brief text to Cloudflare Workers AI, which runs the Clef decision model; no
other request data is sent. Ages count however they are written, in digits, in
words, or in another language. When the brief states a hard age requirement,
every product returned ends its description with this note:
> The age stated in the brief was used to recommend this product but was not
> applied as targeting. To restrict delivery by age, send
> targeting\_overlay.demographics.age on an AdCP 3.2 request.
| Brief wording (no structured age) | Result |
| - | - |
| `Target ages 25-54.`, `Target ages twenty-five to fifty-four.`, `Please target women 18-55.` or `Exclude anyone under 25.` | Products recommended from the brief, each with the note. Delivery is not restricted by age. |
| `Include people whose age is unknown.` | Products with the note; set `include_unknown` in `targeting_overlay.demographics.age` to apply it. |
| `Ideally women 18-55, but this is not a hard requirement.` | Products, no note; a stated preference. |
| `Our customer is generally in her 30s.` | Products, no note; a description of the audience. |
| `Budget 2500 EUR over 20 dagen.` | Products, no note; the number is a duration. |
A request that sends `targeting_overlay.demographics.age` gets that range
applied exactly, whatever the brief says, and no note. A request served at AdCP
3.1 has no structured age field, so until these storefronts negotiate AdCP 3.2
publicly, a 3.1 caller's age wording is used for recommendations only. If the
decision model cannot be reached at that moment, the note is added only for a
narrow set of explicit age phrases, such as `ages 25-54`, `18+ only`,
`adults only` or `minimum age 21`.
**Built-in platform adapters** (the platforms listed below) currently read one
closed age range from the brief:
```text theme={null}
Build an awareness campaign for people ages 21-35 in the United States.
```
The adapter copies the two numbers from the brief. No path invents an age
range, translates a generation label such as “Gen Z” into ages, or widens the
request to fit a platform.
This applies during product discovery and media-buy creation. It does not
change targeting on an existing media buy.
## Platform support
Each storefront must prove that it can execute the requested range exactly.
| Platform | Supported requests |
| - | - |
| Meta | Any closed range from 18 through 64. Meta's provider value `65` means 65+, so it is not an exact maximum. |
| Spotify | Any closed range from 18 through 65. |
| Google Ads | Exact unions of the native 18-24, 25-34, 35-44, 45-54, and 55-64 buckets on the supported Search products. The 65+ bucket is open-ended. |
| Snap | Exact unions of the native 18-20, 21-24, and 25-34 buckets. Open-ended buckets are not treated as a closed maximum. |
| TikTok | Exact unions of the native 18-24, 25-34, 35-44, and 45-54 buckets. The 55+ bucket is open-ended. |
| Pinterest | Exact unions of the native 18-24, 25-34, 35-44, 45-54, and 55-64 buckets. The 65+ bucket is open-ended. |
| Other direct adapters | Exact age targeting is not currently supported unless listed above. Ad-server-backed sellers may expose exact mapped age signals as described below. |
`ages 21-35` therefore works on Meta and Spotify. `ages 25-44` also works on
Google Ads because it is the exact union of two closed native buckets. A range
that cuts through a bucket, or a closed range such as `65-99` projected onto
Google's open-ended `65+` bucket, is never approximated.
## Snap: Australia minimum age
Snap prohibits reaching people under 16 when a media buy targets Australia
exclusively. If a Snap media buy targets Australia only and the brief has not
requested an age range, Apostra automatically applies an 18+ audience
before sending the buy to Snap — so if you target Australia and don't say
18+, Snap will get an 18+ audience. This only applies when Australia is the
sole targeted country; combining Australia with another country does not
trigger it.
If you explicitly request `ages 13-17` on an Australia-only Snap buy, the
media buy is rejected before it reaches Snap rather than having that request
silently overridden.
## Requests that need clarification
A Storefront-built or Agent-supplied storefront rejects a structured age it
cannot read exactly, with `field: "targeting_overlay.demographics.age"`:
* `INVALID_REQUEST` when `include_unknown` is missing, a bound is not a whole
number, no bound is given, `min` is below 18, `min` is greater than `max`, or
`max` is above 120.
* `UNSUPPORTED_FEATURE` when one bound is omitted (an open-ended range such as
21+), or when `accepted_bases` or `accepted_verification_methods` is sent.
A built-in adapter asks for clarification on an ambiguous, excluded, or
open-ended age range in the brief. An exact but unsupported age range receives a
**counter-pitch** instead: a composition
storefront returns products that fit the rest of the brief and includes an
additive `limitations` entry. The proposal is composed from inventory without
age targeting; fixed-age inventory is excluded rather than silently delivering
to a different age audience. Its `message` says this explicitly, `requested`
preserves the requested range, `excluded_ranges` lists fixed ranges omitted
from the counter-pitch, and `supported_ranges` lists the exact ranges the seller
can execute (or is empty when none are declared). The storefront never
broadens, approximates, or inverts the age request.
```json theme={null}
{
"limitations": [{
"code": "age_targeting_unavailable",
"message": "Requested age targeting 35-64 is not available from this Seller; proposal composed from inventory without age targeting. Fixed age ranges excluded: 25-54. Supported ranges: 25-54.",
"requested": { "min": 35, "max": 64, "include_unknown": false },
"excluded_ranges": [{ "min": 25, "max": 54, "include_unknown": false }],
"supported_ranges": [{ "min": 25, "max": 54, "include_unknown": false }]
}]
}
```
For a built-in adapter, rewrite the brief as one explicit, closed, supported
adult range when it contains:
* An open endpoint, such as `ages 21+`, `under 35`, or `up to 35`.
* More than one or a disjoint range, such as `ages 21-24 or 35-44`.
* An exclusion, such as `exclude people ages 21-35`.
* A minimum below 18.
* Conflicting or ambiguous bounds.
* A descriptive label without numbers, such as `young adults` or `Gen Z`.
This fail-closed response is expected. It prevents a storefront or model from
silently choosing a different audience.
## Unknown ages on Meta and Google Ads
Meta and Google Ads can deliver to people whose age is unknown. A
constrained-age request excludes them unless it explicitly says to include
them. In the structured field, set `include_unknown: true`. In a brief, say so
directly:
```text theme={null}
Reach people ages 21-35 and include people whose age is unknown.
```
You can also state that they must be excluded. The choice is stored with the
product, written to the provider targeting object, and checked on readback.
Meta may omit the unknown-age field when its effective value is false. In that
case, readback still preserves your explicit exclusion under the requested
controls and reports an exact match. A provider value of true never satisfies
an exclusion or an age-constrained request that did not opt in.
On Google Ads, every constrained request writes positive criteria for the
selected age buckets and negative criteria for every omitted bucket. This
includes a negative `AGE_RANGE_UNDETERMINED` criterion unless the brief
explicitly includes unknown ages; an include-unknown choice instead writes it
as a positive criterion. Exact readback verifies the complete positive and
negative set. Other platforms reject an include-unknown request unless their
row above says otherwise.
## Read the applied targeting
A returned product with verified age targeting includes the applied range at:
```json theme={null}
{
"ext": {
"scope3_product_targeting": {
"demographics": {
"age": {
"min": 21,
"max": 35,
"includeUnknown": false,
"determinationMethod": "user_verified",
"dataProvider": "Publisher registration data"
}
}
}
}
}
```
Use the returned product for media-buy creation. The storefront checks the stored
product declaration again before publishing it or writing provider state. On
Meta, requested and provider controls also show whether unknown ages were
included.
**Signal-based path:** Age targeting through product-level signals
(described in this guide) is carried by the selected product via
`ext.scope3_product_targeting.demographics`. It is not a
`targeting_overlay` field; the paths are separate.
**AdCP 3.2 overlay path:** `overlaySupport.demographics` is the permission
signal for demographic overlays, and `demographicTargeting` is the companion
product-scoped compiler contract for supported age bounds and unknown-age
handling. `save_media_buy` rejects `targetingOverlay.demographics` before
dispatch while the application seller transport remains on AdCP 3.1. Treat
these fields as
discovery/readback data for now, and use signal targeting or omit
demographics when staging a media buy.
## Seller impact
Sellers using a built-in adapter do not need a new configuration field. A
custom or upstream sales agent must return the exact
`ext.scope3_product_targeting.demographics.age` declaration or its product is
filtered from that brief. As an alternative, an ad-server-backed seller can map
existing ad-server targeting to a first-class age signal:
The product declaration does not have to come from a user-level age signal. A
wholesale product can declare that its inventory itself represents the exact
age cohort—for example, from survey evidence or modeled audience composition—by
including `determinationMethod` and the responsible `dataProvider` beside its
age range. Chef can then select that inventory directly without attaching an
age signal. Descriptive copy such as “great for young adults” is not enough;
the structured bounds and provenance must be present.
```json theme={null}
{
"signalId": "age_25_54",
"name": "Adults 25-54",
"signalType": "age",
"valueType": "numeric",
"targetingDimension": "age",
"range": { "min": 25, "max": 54 },
"ageDetermination": "user_provided",
"dataProvider": "Publisher registration data",
"adapterConfig": {
"kind": "custom_key_value"
}
}
```
Start from the selected ad-server targeting candidate's complete
`adapterConfig`; the abbreviated object above only illustrates the age fields.
The age meaning comes exclusively from the `age` signal type and its `range`.
A signal named “Adults 25-54” without those fields is not treated as an age
signal. Signal Manager supports three semantic types:
* `generic` is ordinary buyer-selectable targeting.
* `age` is buyer-selectable targeting with a required closed adult range and
determination method.
* `property` is inventory addressing only and never enters the buyer-selectable
signal catalog.
Every age signal also declares one determination method:
* `survey_based`: the person reported their age in a survey.
* `assumptive`: the provider inferred or modeled the age.
* `user_provided`: the person supplied their age, without identity verification.
* `user_verified`: the supplied age was verified against an identity source.
`dataProvider` separately names who supplied the dataset. It may name the
seller, a first-party registration system, or a third-party data provider; the
provider name does not substitute for the determination method.
When a request asks for the same closed range, the storefront can select products
from that signal's source and automatically applies the signal on the buy. A
different or partially overlapping bucket is not accepted, and signal-backed
age buckets do not include unknown ages.
Product declarations and mapped age signals describe targeting that will be
applied, not the full range the source might be capable of creating. For
example, a product or signal declaring ages 18-65 does not prove that ages
25-54 will be applied. The source must return a product or explicit signal
declaring the requested range exactly.
## Restrict acceptable age determination
Until AdCP 3.2 supplies native provenance fields, a buyer can add the temporary
Apostra extension below to the same `get_products` request:
```json theme={null}
{
"brief": "Premium display inventory.",
"targeting_overlay": {
"demographics": {
"age": { "min": 25, "max": 54, "include_unknown": false }
}
},
"ext": {
"scope3_age_targeting": {
"accepted_determination_methods": [
"user_provided",
"user_verified"
]
}
}
}
```
When this allow-list is present, the storefront only returns a product whose
exact age mapping declares one of those methods. Missing, unknown, or disallowed
provenance fails closed; it is never treated as approval. A product with age
targeting baked into its inventory metadata declares the same fact as
`ext.scope3_product_targeting.demographics.age.determinationMethod`.
# Buyer Activity
Source: https://docs.apostra.com/v2/guides/audit-logs
Review API calls and domain changes for debugging, compliance, and operational visibility
## Overview
Buyer Activity has two complementary records:
* **Calls** show each admitted buyer API request, including reads, failures, denials, latency, workload identity, and correlation IDs.
* **Changes** show meaningful `CREATE`, `UPDATE`, `DELETE`, `ARCHIVE`, and `EXECUTE` mutations on advertisers, campaigns, creatives, media buys, products, and packages.
Use Calls to debug what an agent attempted. Use Changes to verify what actually changed.
Use audit logs for:
* **Compliance** — review the recorded trail of who changed what and when across multi-tenant workspaces.
* **Debugging** — reconstruct the sequence of events around a campaign that suddenly stopped pacing or a creative that flipped to `rejected`.
* **Post-incident analysis** — answer "what changed in the last hour?" and "did this agent touch anything it wasn't supposed to?".
* **Operational visibility** — feed the data into a SIEM, a Slack channel, or your own dashboard.
Audit logs are scoped to your account. Each row identifies the actor (`userId` + `userEmail` for humans, `serviceTokenId` + `serviceTokenName` for agents), the resource (`resourceType` + `resourceId` + `resourceName`), the action, and the parameters or field-level changes that drove it.
MCP is the primary agent interface. Calls made through MCP and REST appear in the same Activity ledger. The REST endpoints below power the in-app view and let external operational systems query that ledger directly; they are not a separate REST-only activity product.
Apostra internal admin and staff operations are excluded from your account's Activity ledger, including when an Apostra operator is inspecting your workspace.
## Open Activity in your agent client
On the v2 buyer MCP server, call the typed `list_buyer_activity` read—or ask your agent to “show my API activity,” “show failed calls,” or “what did this workload do?” MCP clients that support MCP Apps render the Activity page inside the current conversation, so you do not need to switch to Apostra web app.
```json theme={null}
{
"name": "list_buyer_activity",
"arguments": {
"view": "calls",
"outcome": "failed",
"surface": "mcp"
}
}
```
The page self-fetches authorized data and provides:
* **Calls** — normalized REST, MCP, and A2A operations with workload, outcome, latency, environment, and correlation IDs;
* **Call detail** — recorded request fields and available response or error details, redacted allowlisted headers, validation/response steps, timing, and retry guidance;
* **Ask Murph about this call** — calls the shared typed `ask_murph` tool with the exact `activityUid` and renders the evidence-grounded diagnosis inline, without adding a chat turn; and
* **Changes** — the compatible domain-mutation history previously shown as the entire Activity log.
The typed result includes a compact model-readable summary and the standard `ui://agentic-api/buyer-activity/mcp-app.html` resource directive. Clients without MCP Apps support can still summarize the typed result and query the REST endpoints below. They should not describe the separate Changes feed as a complete API-call history.
## Prerequisites
* An Apostra API key (see [Authentication](/v2/authentication))
* Buyer members can list account-scoped call and change metadata. Account or advertiser admins can open redacted call detail. Service tokens can see only activity attributed to their stable workload lineage.
* Optional: a campaign or advertiser ID to scope the feed
```bash theme={null}
export SCOPE3_API_KEY=scope3_
export BASE=https://api.apostra.com/api/v2/buyer
export API_BASE=https://api.apostra.com/api/v2
```
## Step 1: List recent activity
```bash theme={null}
curl "$BASE/audit-logs?take=50" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```json theme={null}
{
"data": {
"logs": [
{
"id": 81234,
"timestamp": "2026-04-26T14:30:12.481Z",
"createdAt": "2026-04-26T14:30:12.481Z",
"action": "UPDATE",
"resourceType": "CAMPAIGN",
"resourceId": "camp_abc123",
"resourceName": "Q2 Brand Launch",
"parentType": "BRAND_AGENT",
"advertiserId": 42,
"userId": 901,
"userEmail": "alex@brand.example",
"userName": "Alex Operator",
"serviceTokenId": null,
"serviceTokenName": null,
"parameters": { "budget": { "total": 250000 } },
"changes": { "budget.total": { "from": 200000, "to": 250000 } },
"description": "Increased total budget from $200,000 to $250,000"
}
],
"total": 4128
},
"meta": {
"pagination": { "skip": 0, "take": 50, "total": 4128, "returned": 1 }
}
}
```
Each row carries:
| Field | Notes |
| - | - |
| `id`, `timestamp`, `createdAt` | Stable row identifier and event time (ISO 8601, UTC) |
| `action` | One of `CREATE`, `UPDATE`, `DELETE`, `ARCHIVE`, `EXECUTE` |
| `resourceType` | `CAMPAIGN`, `CREATIVE`, `MEDIA_BUY`, `PRODUCT`, `PACKAGE` |
| `resourceId`, `resourceName` | Public ID and human label of the resource that changed |
| `parentType`, `advertiserId` | Hierarchy context (e.g. the advertiser the campaign belongs to) |
| `userId`, `userEmail`, `userName` | Set when a human user made the change |
| `serviceTokenId`, `serviceTokenName` | Set when an agent / service token made the change |
| `parameters` | The input payload for the operation (e.g. the body of an `UPDATE`) |
| `changes` | Field-level before/after diff, populated where applicable |
| `description` | Pre-rendered human-readable summary |
Exactly one of (`userId`, `userEmail`) and (`serviceTokenId`, `serviceTokenName`) is set per row. Use that to distinguish human edits from agent activity.
## Step 2: Filter
The endpoint supports four filter dimensions. Combine them as needed.
| Query param | Type | Meaning |
| - | - | - |
| `startDate`, `endDate` | ISO timestamps | Inclusive time window |
| `advertiserId` | integer | Restrict to a single advertiser |
| `campaignId` | string (e.g. `camp_abc123`) | The campaign row plus all of its descendants (media buys, etc.) |
| `resourceTypes` | repeated or comma-separated enum | Subset of `CAMPAIGN`, `CREATIVE`, `MEDIA_BUY`, `PRODUCT`, `PACKAGE` |
| `take`, `skip` | integers | Pagination — `take` defaults to `50`, max `500` |
```bash Time window theme={null}
curl "$BASE/audit-logs?startDate=2026-04-26T00:00:00Z&endDate=2026-04-26T23:59:59Z&take=200" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```bash Single advertiser, last 24h theme={null}
curl "$BASE/audit-logs?advertiserId=42&startDate=2026-04-25T14:30:00Z" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```bash Creatives + media buys only theme={null}
curl "$BASE/audit-logs?resourceTypes=CREATIVE,MEDIA_BUY&take=100" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
`resourceTypes` accepts either a comma-separated value (`?resourceTypes=CAMPAIGN,CREATIVE`) or repeated query params (`?resourceTypes=CAMPAIGN&resourceTypes=CREATIVE`). The default is the full set of buyer resource types.
The changes feed always restricts to **meaningful actions** — `CREATE`, `UPDATE`, `DELETE`, `ARCHIVE`, `EXECUTE`. Read-only operations are available from the Calls endpoint below.
## Step 3: Common patterns
### Who changed this campaign?
Scope to a campaign and inspect the actor on each row. The `campaignId` filter matches both the campaign itself and its descendants (media buys, etc.) so you see the full activity tree.
```bash theme={null}
curl "$BASE/audit-logs?campaignId=camp_abc123&take=100" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Then, on the client, group by `userEmail || serviceTokenName` to see which operator or agent has been touching the campaign.
### What did this agent do today?
Filter to a 24-hour window and look at rows where `serviceTokenId` is non-null. Match `serviceTokenName` against the agent you care about.
```bash theme={null}
curl "$BASE/audit-logs?startDate=2026-04-26T00:00:00Z&take=500" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
| jq '.data.logs[] | select(.serviceTokenName == "ops-bot")'
```
### Did this incident leave a trace?
For post-incident analysis, pull the time window around the symptom and filter to the resource types most likely to have driven it.
```bash theme={null}
curl "$BASE/audit-logs?startDate=2026-04-26T13:45:00Z&endDate=2026-04-26T14:15:00Z&resourceTypes=CAMPAIGN,MEDIA_BUY" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
The `description` field gives a one-line human summary; `parameters` and `changes` carry the full diff for deeper inspection.
## Debug API calls
`GET /activity/calls` returns the normalized call ledger for your account. It is cursor-paginated and defaults to 50 rows.
The Calls view is available to buyer and seller accounts. Durable call capture begins before anyone opens the view, so opening it does not begin or reset workload history.
```bash theme={null}
curl "$API_BASE/activity/calls?startTime=2026-07-13T00:00:00Z&outcome=failed" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Useful filters include `startTime`, `endTime`, `workloadUid`, `runUid`, `clientRunId`, `advertiserId`, `environment`, `outcome`, `operation`, and protocol (`surface`). Pass the returned opaque `nextCursor` as `cursor` without editing it; cursors are scoped to the account and filter set.
`clientRunId` is a caller-supplied correlation identifier, so use it to group calls rather than as independent provenance. To verify a run, cross-check the server-observed `apiVersion`, `workloadUid`, `runUid`, and the expected time window.
Each call includes:
* `activityUid`, `requestId`, and `traceId` for support and tracing;
* the normalized `operation`, protocol, HTTP status, outcome, and latency;
* `operationDispatches`, a bounded server-observed record of buyer or storefront facade operations selected by a v3 tool, in invocation order;
* the stable `workloadUid` and credential snapshot used for that request;
* an explicit `runUid` when the caller supplied a run ID or eligible W3C trace, plus its `clientRunId` correlation identifier when supplied;
* `mcpHost`, a bounded client-host hint for MCP calls, or `null` when no supported host family was recognized;
* the `apiVersion` observed by the server for the routed request;
* a safe error class, fault domain, and retry disposition when the call failed.
`mcpHost.family` is one of `chatgpt`, `claude`, `codex`, `conductor`, `copilot`, `cursor`, `gemini`, or `vscode`. Its `source` is `mcp_client_info` or `http_user_agent`, and `ruleset` is currently `v1`. Treat this as a non-authoritative diagnostic hint: both protocol client information and the HTTP User-Agent are supplied by the caller and can be spoofed. Raw client names and User-Agent strings are not retained in this field.
`operationDispatches: null` means the call predates dispatch capture, did not use a captured v3 surface, or ended before the observer could produce a complete snapshot. A non-null envelope distinguishes a completed v3 call that selected no facade operation from one whose evidence is unavailable. Preserve duplicate items: retries and fan-outs are recorded as separate attempts. Treat `truncated: true` or an item whose `completionState` is `started` as incomplete evidence. Native v3 service calls that do not use the buyer or storefront facade are outside this field's scope.
Fetch one call with `GET /api/v2/activity/calls/{activityUid}`. For newly captured calls, the detail response adds:
* `redactedInput`: schema-declared MCP arguments, or schema-validated REST path, query, and body fields, after sensitive values are removed;
* `redactedOutput`: the redacted typed MCP result, or the public REST error envelope returned to the caller (successful REST payloads are not copied into the ledger);
* `safeHeaders`: an explicit allowlist of non-secret debugging headers, such as content type, idempotency key, user agent, request ID, and run ID;
* `steps`: validation and response milestones, including field paths and public validation messages; and
* route, timing, workload, run, trace, and retry guidance.
Unknown MCP arguments and unknown or invalid raw REST fields are not stored. Authorization, cookies, credentials, arbitrary headers or protocol baggage, successful REST response bodies, internal stack traces, and provider costs are never captured. Diagnostic detail expires after 90 days; slim call metadata and correlation IDs remain available for 13 months.
In the native or portable Activity page, select a call and choose **Ask Murph about this call**. The Page calls the buyer MCP server's typed `ask_murph` tool with the exact `activityUid`, then shows the answer or retry state inline. The action does not submit a synthetic user message or modify the host composer, so it behaves the same way in Apostra, Claude, ChatGPT, and other MCP-app hosts. Murph explains only what the evidence supports and can point to the relevant documentation. It cannot bypass account, workload, or private-conversation access rules. You can also paste an `X-Scope3-Activity-Id` into an authorized Murph conversation.
### Group calls into an agent run
For MCP, send the same namespaced metadata on each `tools/call` that belongs to one explicit agent cycle:
```json theme={null}
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "api_call",
"arguments": { "operation": "campaigns.list" },
"_meta": {
"scope3/run-id": "nightly-reporting:2026-07-14",
"scope3/run-label": "Nightly reporting"
}
}
}
```
Every admitted MCP response includes `X-Scope3-Activity-Id`. A normal tool result also includes the same correlation in result metadata:
```json theme={null}
{
"_meta": {
"scope3/activity": {
"activityUid": "e691bf1a-2f17-4eec-9394-e647dcfe994d",
"requestId": "0c240930-4e42-4a7f-8716-7329c862932f",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"runUid": "421ea561-69fc-46ee-93c3-b8e2676ef603"
}
}
}
```
For REST, send the equivalent headers:
```bash theme={null}
curl "$BASE/campaigns" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "X-Scope3-Run-Id: nightly-reporting:2026-07-13" \
-H "X-Scope3-Run-Label: Nightly reporting"
```
Run IDs accept 1–128 characters from letters, numbers, `.`, `_`, `:`, and `-`. Run labels are optional and limited to 120 characters. When no run ID is present, a valid caller-supplied W3C `traceparent` root trace ID can group workload calls; server-created traces never manufacture a run.
### Correlate a skill workflow
Use optional workflow annotations when one run executes a repeatable skill or acceptance scenario. They work on ordinary calls, including `/mcp/v3`; they do not require a sandbox or a special test endpoint.
For MCP, add the annotations to the same `_meta` object as the run ID:
```json theme={null}
{
"_meta": {
"scope3/run-id": "sales-agent-check:2026-08-25",
"scope3/workflow-id": "validate-agent",
"scope3/skill-id": "test-sales-agent@1.5.1",
"scope3/scenario-id": "own-supply-transaction",
"scope3/step-id": "stage-media-buy",
"scope3/intent-hint": "agent_testing"
}
}
```
REST uses the equivalent headers: `X-Scope3-Workflow-Id`, `X-Scope3-Skill-Id`, `X-Scope3-Scenario-Id`, and `X-Scope3-Step-Id`. If a header and MCP metadata specify the same annotation, the header wins. Each identifier is limited to 128 letters, numbers, dots, underscores, colons, at signs, or hyphens; use opaque, non-sensitive values rather than names or customer data.
The optional intent label uses `X-Scope3-Intent-Hint` for REST and
`scope3/intent-hint` for MCP. It accepts 1–64 lowercase letters, digits, or
underscores, must begin with a letter, and is a correlation hint rather than
an authoritative intent classification.
These values are caller-supplied diagnostic annotations. Valid values are stored in dedicated Activity fields and forwarded on downstream HTTP calls so a buyer-side workflow can be matched to seller-side logs. They never grant access, establish certification, or replace the server-issued `activityUid`, `requestId`, or `traceId`. Use those server-observed fields and `operationDispatches` when verifying what actually ran.
Agent validation diagnostics apply the same rule. A workflow appears in
`get({"kind":"agent","include":["validationRuns"]})` only after durable server
evidence binds the Activity run to that exact Agent revision. The supplied skill,
scenario, workflow, and step IDs describe the ordered graph; they do not create
that binding or expand who may read it.
Every durably admitted request returns `X-Scope3-Activity-Id` alongside `X-Request-Id` and `X-Trace-Id`. If the activity ledger cannot persist the start, the API returns `503 ACTIVITY_LEDGER_UNAVAILABLE` and does not run the requested operation.
## Best practices
**Pagination** — the endpoint orders by `timestamp DESC`. Page with `skip` + `take`; for large back-fills, iterate by time window (`endDate` of one page becomes `startDate` of the next) instead of paging deep into a single window.
**Polling cadence** — for live tailing, poll once per minute and use `startDate` set to the most recent `timestamp` you've already ingested. Do not poll faster than once per 30 seconds.
* **Retention** — safe call diagnostics are retained for 90 days and slim call metadata for 13 months. Archive records you need longer to your own warehouse.
* **Pagination ceiling** — `take` is capped at `500`. For larger exports, iterate by time window.
* **Idempotency** — `id` is stable per row, so client-side dedupe on `id` is safe across overlapping polls.
* **Don't rely on `actions` filtering at the API** — the activity feed always restricts to meaningful actions; filter further client-side if you need to (e.g. only `DELETE`).
## Endpoint reference
| Method | Path | Purpose |
| - | - | - |
| `GET` | `/audit-logs` | List buyer activity, with optional filters and pagination |
| `GET` | `/api/v2/activity/calls` | List normalized API calls for the authenticated tenant |
| `GET` | `/api/v2/activity/calls/{activityUid}` | Inspect one authorized API call |
The equivalent typed MCP read is `list_buyer_activity`. It owns the portable Activity page; there is no separate `open_activity` launcher.
**Query parameters**: `startDate`, `endDate`, `advertiserId`, `campaignId`, `resourceTypes`, `take` (default `50`, max `500`), `skip` (default `0`).
**Response**: `{ data: { logs: BuyerAuditLog[], total: number }, meta: { pagination: { skip, take, total, returned } } }`.
See the OpenAPI spec for the full `BuyerAuditLog` shape: [API Reference](/v2/buyer-api-reference).
## Related
* [Notifications](/v2/guides/notifications) — push notifications for the same underlying events
* [Errors](/v2/reference/errors) — error codes you may see surfaced in `parameters`
* [Authentication](/v2/authentication) — required role and token setup
# Importing From Cloud Drives
Source: https://docs.apostra.com/v2/guides/cloud-drive-connections
Choose Google Drive files for Murph and creative upload workflows
## Overview
Cloud drive imports let a signed-in user choose files from cloud storage,
starting with Google Drive. They are designed for creative and operations work
where briefs, images, videos, and review materials already live in a shared
drive.
Google Drive is available directly from the Murph composer and campaign
creative upload workflow. It does not require a separate setup step in
Connections. Each import opens a Google-owned selection window where the user
authorizes and chooses the files for that import.
Cloud drives are separate from ad platform connections. Connecting Google Drive
does not grant Apostra access to Google Ads, Google Ad Manager, a storefront, or
an ADCP sales agent. It only authorizes the cloud-drive file workflow for the
signed-in user.
## Google Drive
Apostra requests file-scoped Google Drive access. Each selection happens in a
top-level Google window, so it works without third-party cookie access. Apostra
can work with files the user selects for Apostra in a supported workflow, not
broadly index or manage the user's entire Drive.
The selected-file workflow does not create a persistent Google Drive
connection in Apostra. Google asks the user to authorize access in the
selection window each time files are added.
## What It Enables
Google Drive import lets signed-in users:
* Buyers can choose files from Google Drive while bulk-uploading campaign
creatives.
* Buyers and sellers can choose **Add from Google Drive** from the `+` menu in
Murph and attach the selected files to a message.
* Preview, group, and validate imported files through the same workflow used
for local files, folders, and zip archives.
For buyer accounts, import campaign creatives by opening the campaign's
**Creative assets** view, choosing **Bulk upload**, and selecting **Google
Drive**. Apostra downloads only the files selected in the Google-owned window
and applies the normal creative file limits.
In Murph, choose **Add from Google Drive** from the composer `+` menu. The
Google selection window opens without leaving the conversation, so the text
draft stays in place. Selected files become ordinary message attachments.
## Requesting Other Cloud Drives
**Connections → Cloud drives** lists curated cloud drives that are not available yet,
including Microsoft OneDrive, Dropbox, and Box. Choose **I want this** to record
your organization's interest. The request does not create a connection or
grant Apostra access to that provider.
## What It Is Not Used For
A cloud drive connection does not:
* Connect an advertiser account or ad platform account.
* Keep ongoing access to a Google Drive account.
* Browse, index, map, or watch Drive folders in the background.
* Give Apostra permission to manage users, sharing settings, or billing in
Google Workspace.
* Turn Connections into a marketplace for arbitrary MCP servers.
* Authorize Apostra to write generated files back to Drive unless a future
workflow explicitly asks for that capability.
Persistent folder access, monitoring, and source-file mapping are not part of
selected-file import. A future sync workflow would require a separate,
explicit connection and broader read permission.
# Connecting Ad Platform Accounts
Source: https://docs.apostra.com/v2/guides/connecting-ad-platforms
Grant Apostra access to advertiser accounts on Meta, TikTok, Google Ads, and other ad platforms
## Overview
Some storefronts run through direct ad platform adapters instead of a publisher
sales agent. Before Apostra can discover products, create media buys, sync
creatives, or read delivery from those adapters, the customer must connect the
relevant advertiser account to Apostra through the platform's own account-sharing
or OAuth flow.
This guide is written for legal, procurement, and platform admins who need to
approve the access request. For what a connection actually is — organization
vs advertiser accounts, manager-account (MCC / Business Manager) expansion,
account types, and how discovered accounts map to your advertisers — see
[Ad Platform Accounts](/v2/concepts/ad-platform-accounts).
When Claude, ChatGPT, or another generic MCP client operates a connected
account, it can call `get_adcp_tool_schema` before a write. Start with the tool
name, then inspect the returned nested paths—for example,
`packages[].targeting_overlay` before `create_media_buy`, and `revision` plus
`packages[]` before `update_media_buy`. This returns the exact versioned schema
fragment on demand without loading every platform contract into the initial
tool list. Clients should inspect those fields rather than guess targeting,
creative, budget, or lifecycle request shapes.
This page covers ad platform account access such as Meta, TikTok, Google Ads,
Snap, LinkedIn, Pinterest, Reddit, OpenAI Ads, and Amazon Ads. Google Ads is
separate from Google Ad Manager. For Google Ad Manager inventory-source setup,
see the [Storefront onboarding
guide](/v2/setup/storefront-onboarding#google-ad-manager-service-account-grant).
## Product Discovery Modes
Direct ad-platform adapters expose two different product-discovery jobs. Keep
them separate:
* `buying_mode: "wholesale"` returns the adapter's **certified executable
product archetypes** currently available for the selected account. It does not
interpret a brief, rank or truncate products, read campaign performance,
generate a forecast, or create a proposal. Product order and IDs remain
stable while the connected account and deployed catalog are unchanged, and
`wholesale_feed_version` identifies the exact returned feed.
* `buying_mode: "brief"` is the recommendation path. It may interpret the
advertiser's ask and use account evidence where that adapter supports it.
Missing campaign decisions or account setup can therefore produce
clarification requirements even when wholesale mode shows available
products.
For Snap and TikTok, objective interpretation is semantic rather than a phrase
match. The interpreter must identify one supported goal and quote the exact
brief text that supports it; deterministic validation then maps that goal to an
executable catalog product. Missing, conflicting, ungrounded, or unsupported
goals return a clarification instead of a recommendation.
Wholesale does not mean “ready to launch,” and a product is not every possible
targeting permutation. A wholesale product represents a materially different
provider launch choice: its objective, conversion location or destination,
inventory/campaign type, property or distribution contract, optimization and
buying contract, creative and publication contract, and measurement contract.
For example, lead campaigns that use an instant form versus a website form can
be separate archetypes because their required assets, destination,
measurement, and optimization differ.
Buyer-managed first-party audiences created through `sync_audiences` use
`targeting_overlay.audience_include` and `audience_exclude`. Seller- or
platform/source-native **signals**, such as a LinkedIn professional audience or
a provider-account audience discovered as a signal, use
`targeting_overlay.signal_targeting_groups`. They are discovered through
`get_signals` or a product's inline signal options. The wire representation is
determined by the resource's provenance and lifecycle, not merely by whether a
marketer calls it an audience. Event sources, catalogs/product sets, native
forms or lenses, actor identities, and destinations are separate **activation
resources**. Geography, demographic bounds, device, placement, frequency,
budget, and dates are campaign overlays. A runnable plan combines one
archetype with compatible signals, resources, and overlays; the wholesale
catalog does not multiply all combinations into separate products.
LinkedIn professional attributes are currently inventory-only for ordinary
buyers: their deployments report `is_live: false`, LinkedIn products do not
offer signal targeting, and ordinary create requests containing a signal
cohort fail before LinkedIn I/O. A protected no-spend conformance path retains
the write/readback machinery until exact-revision provider evidence permits a
separate activation change.
In TikTok brief mode, you can explicitly name an existing account audience to
include or exclude. Apostra attaches it only when one exact normalized
signal name or canonical synced-audience ID is live on the selected account.
An unknown, ambiguous, foreign-account, non-live, or contradictory reference
returns a clarification before any provider write. Descriptive audience intent
such as “executives” remains provider signal or targeting intent; Apostra
does not assume it names one of your account audiences.
Wholesale discovery does not call a composition model or create a composition
meter record. Brief mode can use model-backed composition, so provider usage
and one successful composed-response outcome are attributed to the
authenticated buyer for cost calibration. These records currently charge zero
IUs and do not debit the buyer's wallet. The storefront owner is not charged
for buyer-requested brief composition.
For a Meta Instant Form Leads campaign, the Page is the authorized actor, the
Instant Form is the native destination asset, and the platform-native Instant
Forms event source supplies the lead outcome used by the optimization goal.
The Instant Form Conversion Leads product uses the distinct `QUALITY_LEAD`
optimization with the same Page/Form ownership checks. CRM feedback through
Meta's Conversions API is recommended but is not required to create that buy,
and eligible image or video creative remains a separate format choice.
For a website Leads campaign, the website form is the destination while a
Pixel/Dataset or CAPI integration is the event source.
For Meta App Promotion, installs, post-install events, and in-app value are
three wholesale products because they store different native optimization
contracts. The selected app is both the `application` and `event_source`
resource; Meta's app-specific `advertisable_app_events` inventory supplies the
separate `conversion_event`. The Facebook Page is the creative actor, and
provider or synced audiences remain independent signals. Apostra rejects
an unauthorized app/event pair before creating a campaign and requires exact
application, event, and value-goal readback. When Meta exposes multiple native
names for the same canonical event, the generated execution package preserves
the selected account value in `ext.meta_app_event_name`; choose that value from
account readiness rather than inventing a provider event name.
For **Meta Sales — Website Catalog Sales**, the wholesale product represents a
manual-auction website catalog-sales campaign. The selected commerce catalog,
non-empty product set, Pixel/Dataset, `PURCHASE` event, Facebook Page, HTTPS
destination, and source creative are activation resources attached to that
product. Audience signals and targeting overlays remain optional, independent
choices; Apostra does not publish one wholesale product for every catalog ×
product set × audience × geography combination.
For Meta website Sales products on the authorized AdCP 3.2 path, Advantage+
is an automation contract on the existing website conversion,
conversion-value, or catalog archetype—not a duplicate wholesale product.
`seller_optimized` moves budget allocation to the campaign, while Advantage+
audience and placements let Meta expand delivery from the supplied inputs.
Included provider-account and buyer-synced audiences are therefore
optimization suggestions, not hard delivery boundaries. Country targeting
and custom-audience exclusions remain explicit controls. Apostra starts
the campaign paused and accepts it only after the campaign budget, Pixel
event, audience inputs, automation state, and omitted manual placements read
back exactly. Ordinary and legacy products continue to disable audience
expansion. When Meta reports its bounded age, gender, or geo automation
settings on a managed package, Apostra preserves them in
`ext.meta_provider_targeting_controls.targeting_automation.individual_setting`.
Active settings are accepted only when the package's durable product is a
recognized integrated Advantage+ Sales or Leads product and its stored request
and create confirmation both authorize Advantage+ audience expansion; a live
Meta field cannot authorize itself. Existing requests require no changes.
For TikTok App In-App Event optimization, the application and conversion event
are separate activation resources. Apostra reads the advertiser's apps,
then the provider-ready events for each app. Every event choice identifies its
owning app and counting mode. Creation rejects an event from another app or an
event that is not ready for the product's bidding contract, and verifies the
exact app, event, and counting mode after TikTok stores the ad group.
TikTok App Retargeting uses the same app-scoped event selection but a distinct
campaign product for existing users. Apostra queries event eligibility in
the retargeting context and verifies that TikTok stored `APP_RETARGETING`.
Custom and app-activity audiences remain signals that you can include or
exclude on top of that product.
For value-based re-engagement, TikTok App Retargeting Value with Maximum
Delivery is a separate wholesale product. Select the advertiser app and one
provider-ready retargeting event that reports value and currency, then attach
an app-activity, provider-account, or buyer-synced audience as a signal.
Apostra first reads the advertiser's authoritative audience inventory; an
available app-activity or customer audience satisfies the prerequisite, while
a lookalike or unrelated provider audience does not. It then verifies that the
event supports Maximum Delivery, saves `VALUE` with the exact event and
counting mode, and does not substitute App Install VBO eligibility. TikTok's
Cost Cap choice remains visible as an explicit capability gap until its exact
API bid and readback contract is certified.
TikTok App Install Purchase Value has distinct Highest Value, Day 0 Target
ROAS, and Day 7 Target ROAS wholesale products. Before offering one for
execution, Apostra checks the selected app's matching Value-Based
Optimization eligibility. Each Target ROAS product requires a positive
return-on-ad-spend target and verifies TikTok's stored Purchase value goal,
numeric target, and exact Day 0 or Day 7 value window; Highest Value has no
buyer-authored target. Apostra refuses to substitute another bid mode or
measurement window when the selected contract is unavailable.
For ad-supported apps, TikTok App Install Ad Revenue Value is available as four
separate products: Highest Value or Target ROAS, each measured over Day 0 or
Day 7. They require real-time impression-level ad revenue postback through a
supported mobile measurement partner and an app TikTok marks eligible for the
exact selected VBO mode. Target ROAS requires a positive return target.
Apostra verifies the saved ad-revenue event, bid strategy, numeric target
when applicable, and exact value window.
**TikTok Sales — Website Catalog Ads** covers TikTok's manual Website Catalog
Ads path. It is one wholesale product, not a separate product for every
catalog, product set, audience, and targeting choice. The current safety
boundary requires one admin-accessible TikTok Business Center that exclusively
owns the advertiser. The catalog, its selected product set, and a Pixel
explicitly bound to that catalog are activation resources. Apostra offers
only eligible catalogs and product sets that meet TikTok's four-product
minimum, then verifies the exact manual campaign, Video Shopping Ads binding,
product set, Pixel, Purchase optimization, and uploaded or Spark video before
reporting success. Provider-account and synced first-party audiences can still
be included or excluded independently, with geography and age applied as
targeting overlays. Smart+ Catalog and TikTok Shop Product or LIVE GMV Max are
separate products and are not included in this manual path.
TikTok Smart+ Catalog Conversion and Purchase Value are tracked as explicit
blocked provider leaves. Their native hierarchy implementation remains dormant
and they are not returned by product discovery until TikTok event statistics
can prove the selected catalog-bound Pixel's Purchase event and, for Value, its
value and currency eligibility. The dormant path verifies the campaign's exact
ecommerce catalog type and complete activation graph. Smart+ Catalog App
prospecting and retargeting are separate, currently non-executable provider
flows.
TikTok Smart+ Traffic is available as two wholesale products: **TikTok Smart+
Traffic — Clicks** and **TikTok Smart+ Traffic — Landing Page Views**. These are
campaign products rather than targeting options because each selects a
different native optimization. Geography, age, gender, and language remain
targeting overlays. Provider-account and buyer-synced audiences remain signals.
TikTok identity, destination, and creative remain activation resources. A
Smart+ asset group may use uploaded video or image assets, or Spark posts, but
cannot mix Spark and non-Spark assets in one buy. Smart+ Web Conversion and
Purchase Value remain blocked and undiscoverable until TikTok event statistics
can prove the selected Pixel event and its value/currency eligibility.
**TikTok Smart+ App — Install** is the automated app-acquisition product. Select
one authorized advertiser app; Apostra detects whether the app is iOS or
Android, applies the matching OS constraint, creates the Smart+ hierarchy
paused, and verifies Install optimization, automatic placement, and the exact
app and asset group before reporting success.
Choose **TikTok Smart+ App — In-App Event** to optimize for one post-install
event that TikTok reports ready for the selected app. Apostra preserves
TikTok's Every or Once event-counting setting, requires the event to support the
product's Maximum Delivery bidding, and verifies the exact event, counting
setting, app, operating system, automatic placement, and Smart+ asset group.
Smart+ App Value is available as seven wholesale products: Purchase Value with
Highest Value, Purchase Value with Day 0 or Day 7 Target ROAS, and Ad Revenue
Value with Highest Value or Target ROAS for Day 0 or Day 7. Select the product
that matches the value event, bid strategy, and measurement window you would
choose in TikTok Ads Manager. Apostra inventories only apps TikTok reports
eligible for that exact contract. Target ROAS products also require a positive
ROAS target. Creation verifies TikTok stored the exact Purchase or
impression-level ad-revenue event, `VO_HIGHEST_VALUE` or `VO_MIN_ROAS`, value
window, and ROAS bid before reporting success.
Across Smart+ App products, the app, TikTok identity, event when applicable,
and uploaded or Spark assets are activation resources. Provider-account and
synced first-party audiences remain optional signals, and geography and age
remain targeting overlays. These resources, signals, and overlays attach to the
selected product; they do not create extra catalog rows.
Smart+ buys remain native after creation. Media-buy lists combine the
advertiser's manual and Smart+ campaigns, and Smart+ pause, resume, budget,
bid, end-date, targeting, package-status, and cancellation changes use TikTok's
dedicated Smart+ hierarchy. Apostra rejects a campaign ID that appears in
both TikTok namespaces rather than guessing which one to change, and confirms
the resulting hierarchy before reporting an update or cancellation.
Use the returned declarations and the platform capability, audience, creative,
and event-source operations to check prerequisites before a write. Discovery
alone does not guarantee activation: a capability must declare whether the
current create path can attach it.
The current certification checks completeness against each adapter's execution
registry, not the provider's entire UI. It proves that every returned archetype
maps to an implemented create path; it does not prove that every workflow in a
provider's Ads Manager has been modeled. Treat the current set as a certified
executable seed while provider-UI parity remains uncertified.
`ext.scope3_catalog_coverage` reports the executable manifest version,
`coverage_basis`, `provider_ui_coverage_status`, the product-archetype, signal,
activation-resource and targeting-overlay axes, and any known provider
configurations that are intentionally blocked. A blocked configuration is not
returned as buyable until its creative, catalog, provider-readback, and
publisher-scope paths are certified end to end.
The direct-adapter catalogs currently use prices maintained in Apostra's
deployed product templates rather than live provider rate-card reads. Each
wholesale product labels that fact in
`ext.scope3_wholesale_catalog.pricing_provenance` with
`source: "adapter_static_template"`, `authority: "scope3"`, and
`freshness: "deployment"`. Treat these values as Apostra catalog guidance, not
as proof of a provider-published rate. Spotify returns only products whose
delivery can be disclosed entirely within Spotify's published property and
placement contract.
## What Write Access Means
When Apostra asks for platform write access, it is asking for permission to
operate campaigns and related advertising objects for the advertiser accounts
that the customer explicitly grants. It does not mean Apostra needs broad
business administration rights.
Write access may be used to:
* Create, update, pause, or resume campaign objects, ad groups or ad sets, ads,
budgets, targeting, and flight dates.
* Upload or sync creative assets and create platform-native creative records.
* Create or update first-party audience segments when the customer enables
audience sync.
* Create or update product catalogs or feeds when the media-buy workflow uses
catalog products.
* Read delivery, spend, approval status, pacing, and performance reporting.
* Send conversion or event data when measurement is configured for the
advertiser.
* List connected advertiser accounts so the customer can choose the right one.
Apostra does not need permission to:
* Add, edit, or delete users.
* Take ownership of the customer's business account or advertiser account.
* Manage unrelated advertiser accounts that were not shared with Apostra.
* Change security settings, login settings, business verification, or platform
account ownership.
* Manage billing users or finance roles unless a customer has a separate,
explicitly approved billing workflow.
## Buyer-Selected Optimization Goals
Connected-platform products advertise the exact `optimization_goals` they can
apply. Apostra rejects malformed or product-incompatible metrics, events,
durations, and targets before creating provider objects. Provider-owned event
source identities are preflighted when the platform exposes authoritative
inventory; otherwise the provider validates them during creation, and a
rejected or mismatched response fails the create and triggers cleanup.
Apostra also verifies the provider's returned optimization field before
creation can complete.
| Platform | Buyer-selectable goals |
| - | - |
| Meta | Reach, clicks, completed views, provider-authorized website/app events, and maximize-value goals on compatible products |
| TikTok | Reach, clicks, 6- or 15-second completed views, catalog-bound website events, and provider-ready app-scoped custom events on compatible products |
| Snap | Clicks, 2- or 15-second completed views, and supported Pixel purchase, add-to-cart, or registration events |
| Pinterest | Clicks and supported Tag purchase, add-to-cart, lead, or registration events; exact CPA targets on compatible conversion products |
| LinkedIn | Clicks for Website Traffic and 2-second completed views for Video Views |
| Google Ads | Clicks for Search Traffic |
| Spotify | Reach, clicks, and 3-second completed video views on compatible products |
| Reddit and Amazon Ads | Not currently buyer-selectable; non-empty requests are rejected |
Per-auction bid ceilings are pricing controls, not optimization goals. They are
not inferred from `cost_per` or other optimization targets. Provider-confirmed
goals are not yet echoed on package status because the current AdCP package
response schema has no `optimization_goals` field.
## Recommended Access Pattern
Use the platform's partner or manager-account sharing flow where available. The
customer remains the owner of the business and advertiser accounts, and can
remove or reduce Apostra's access from the platform console at any time.
For activation workflows, choose the least broad role that can both manage ads
and read reporting. Use read-only access only for reporting-only pilots.
| Use case | Recommended access |
| - | - |
| Reporting only | Read/reporting role for the advertiser account |
| Campaign activation | Campaign/ad management role plus reporting |
| Creative sync | Campaign/ad management role plus access to required creative identity assets |
| Audience sync | Campaign/ad management role plus audience/customer-list permissions |
| Catalog products | Campaign/ad management role plus catalog/feed permissions |
## Agencies: Link the Manager Account
When an agency runs media for several clients, the pattern is the same on every
platform:
1. Each client grants access on the **platform side** — to the agency's
container (Business Manager, manager account / MCC, Business Center) or, on
platforms without an organization container, to the agency's user.
2. The agency connects **its own login** in Apostra, once.
3. Every client account the grant reaches is discovered under that one
connection, grouped under the manager account where the platform has one.
4. The agency maps each client account to the matching advertiser. In the web
app, use the **Advertiser mapping** tab under **Settings → Connections**. In
the Connections widget, use **Advertiser mappings** inside the connected
storefront's card.
Organization-level linking is the agency-correct move because one grant
surfaces the whole client portfolio. Linking a single client-shared ad account
also works, but each additional client then needs its own connect. The
per-platform sections below include an **Agency setup** subsection with the
client-side steps.
## Platform Summary
| Platform | Default customer grant | Read-only fallback | Usually not required |
| - | - | - | - |
| Meta | Partner access to the ad account with campaign management and performance reporting | View performance only | Full business portfolio control, billing admin, user management |
| TikTok | Business Center partner access to the advertiser account as Operator | Analyst | Business Center Admin, finance roles, user management |
| Google Ads | Manager-account or account access that can manage campaigns and read reporting | Read-only reporting role | Billing admin, user management |
| Snap | OAuth or partner/account access with ads management and reporting | Reporting-only role when supported | Organization admin, user management |
| LinkedIn | OAuth or ad account access with ads read/write and reporting | Reporting-only role when supported | Company Page admin unless organic/social posting is required |
| Pinterest | OAuth or ad account access with ads read/write and reporting | Reporting-only role when supported | Account ownership or user administration |
| Reddit | OAuth or ads account access with ads read/write and conversion permissions when measurement is enabled | Reporting-only role when supported | Profile ownership, user administration |
| Spotify | OAuth access from a user who can manage campaigns and read reporting for the advertiser account | Reporting-only access when supported | Business ownership, billing admin, user management |
| OpenAI Ads | OAuth access or an API key scoped to the advertiser account | None | OpenAI organization administration or user management |
| Amazon Ads | Advertising account access with campaign-management permission | Reporting-only role when supported | Retail account administration, billing admin |
## Targeting Support Differs by Platform
Every platform accepts a different subset of `targeting_overlay`. Ask the
platform rather than assuming: `get_adcp_capabilities` returns
`media_buy.execution.targeting`, which lists exactly the dimensions that
platform applies.
On `create_media_buy` that declaration is enforced, not advisory:
* **A dimension the platform declares is applied.** If it appears in
`media_buy.execution.targeting`, the campaign delivers against it.
* **A dimension the platform does not declare is refused.** The request fails
with HTTP 400 and error code `UNSUPPORTED_TARGETING_DIMENSION`, naming the
dimension and the package it came from. Nothing is created — the rejection
happens before the platform is called at all, so there is no half-built
campaign to clean up.
A targeting request is never accepted and then ignored. If a call succeeds, every
dimension you sent is in effect; if a dimension cannot be honored, you get an
error instead of a campaign that quietly delivers to the wrong audience. When you
need a dimension a platform refuses, either drop it or use a platform whose
capabilities include it.
### Age targeting from a brief
Age is currently product targeting, not a `targeting_overlay` dimension. Apostra
uses only numeric bounds written explicitly in the brief and applies an 18+
policy floor. It does not ask the model to invent bounds or approximate an
unsupported request.
| Platform | Brief age behavior |
| - | - |
| Meta | Exact inclusive bounds from 18 through 64. Meta's provider value `65` means 65+, so it is not treated as an exact closed interval. |
| Spotify | Exact inclusive bounds from 18 through 65. |
| Google Ads | Exact unions of the native 18-24, 25-34, 35-44, 45-54, and 55-64 buckets for the supported Search products. The 65+ bucket is open-ended. |
| Snap, TikTok, Pinterest | Exact native bucket unions only; a request cutting through a bucket is refused. |
| Other connected adapters | Constrained-age briefs are refused. |
For example, `ages 21-35` is exact on Meta and Spotify, but is
not exactly representable by the current Google Ads, Snap, TikTok, or Pinterest buckets.
Open-ended, disjoint, ambiguous, negated or exclusionary, or below-18 ranges return no actionable
product. A seller or upstream sales agent can make an age-baked product eligible
by returning Apostra's exact product targeting declaration; Apostra composes only
products whose declared bounds match the brief and never guesses the seller's
mapping. Composed products carry that declaration forward. Refining several
products preserves age only when every selected product declares the same exact
target; conflicting or undeclared targets are not combined.
Unknown-age delivery is excluded by default for any constrained-age request.
Meta and the supported Google Search products also accept an explicit request
to include or exclude people whose age is unknown; other adapters reject an
include-unknown request.
For supported wording, provider boundaries, and response fields, see
[Exact age targeting](/v2/guides/age-targeting).
### Changing targeting on an existing buy
`update_media_buy` is narrower than `create_media_buy`, and the difference is
per-platform:
| Platform | Changing `targeting_overlay` on an existing buy |
| - | - |
| Meta | Geography (`geo_countries`, `geo_regions`, canonical AdCP 3.2 `geo_places`, and legacy `ext.scope3.geo_places` cities on pre-3.2 requests) and Custom or Lookalike Audience inclusion and suppression (`audience_include`, `audience_exclude`) can be changed. Other dimensions are refused with `UNSUPPORTED_TARGETING_DIMENSION`. |
| Snap, TikTok, Reddit, Pinterest, LinkedIn, Spotify | Geography (`geo_countries`, `geo_regions`) can be changed. Other dimensions are refused with `UNSUPPORTED_TARGETING_DIMENSION`. |
| Google Ads | Customer Match inclusion and suppression (`audience_include`, `audience_exclude`) can be changed on supported Search and Display buys. Geography remains create-only. Other dimensions are refused with `UNSUPPORTED_TARGETING_DIMENSION`. |
| OpenAI Ads | Budget, bids, geography, custom-audience targeting, context hints, product-set filters, creative assignment, pause, activation, and archive are mutable. Updates validate the selected campaign hierarchy and require exact provider readback. |
For every storefront, Apostra evaluates exact ISO subdivision evidence before
dispatch when the seller publishes it under
`ext.scope3.storefront.geo_region_evidence`. A value the seller explicitly
marks unsupported is refused before the seller can create or change spend.
Inclusion and exclusion are evaluated independently. Evidence is accepted only
when its host-stamped provider, storefront source, and provider account match
the account that will execute the buy and its catalog observation is still
fresh; missing or mismatched evidence falls through to the seller's normal
execution-time validation. External agent sources use the independently known
`adcp` provider boundary rather than trusting a platform name from the evidence
document. AdCP 3.2 supports structured country/value declarations on
`media_buy.execution.targeting.geo_regions` and `geo_regions_exclude`.
Apostra-hosted external Storefront endpoints currently negotiate AdCP 3.0/3.1,
so they keep exact value evidence in the namespaced extension until those
endpoints expose the 3.2 shape. A `geo_regions: true` declaration remains
compatible, but does not by itself prove that a specific state, province,
département, or other subdivision is supported. The seller's own
execution-time validation still applies.
Directly bound modular and manual/HITL inventory sources are stricter today:
their runtime contract cannot prove exact subdivision execution, so a
non-empty `geo_regions` or `geo_regions_exclude` request is refused before
approval, reservation, or dispatch at that storefront instead of being
accepted and silently ignored. Updates also refuse explicit empty arrays when
the runtime cannot prove it will clear the dimension. Linked storefront chains
require recursive terminal-source preflight in a later rollout slice; a linked
relationship does not imply support. Feed transport, product geography,
billing, and approval mode do not establish support.
Pinterest-connected storefronts publish an exhaustive inclusion catalog from
Pinterest's current GEO targeting options. The catalog contains only canonical
ISO 3166-2 identifiers that Pinterest accepts unchanged on create and update,
and it is isolated to the authenticated storefront source and provider account.
An ISO subdivision omitted from that current Pinterest inclusion catalog is
refused before dispatch. Pinterest exclusion support is not inferred from the
inclusion catalog; without independent exclusion evidence, exclusion continues
through the normal execution-time validation path.
Meta-connected storefronts publish non-exhaustive, source/account-scoped live
evidence for French régions after verifying every required legacy Meta region.
A current région may map to several provider keys, and exact readback requires
the whole set. French départements remain `geo_regions` on the buyer wire:
where Meta has no département object, Apostra uses a complete La Poste
postal composition only when no code crosses the requested boundary.
Cross-boundary cases and all overseas subdivisions are refused; the latter
remain unproven under Meta's separate overseas country semantics. On negotiated
AdCP 3.2 calls, configured-product discovery resolves a Meta city to a stable
numeric key and returns it in canonical `geo_places`, including the exact
catalog version. Pre-3.2 calls use `ext.scope3.geo_places` with `country`,
`name`, and `place_type: "city"`. The forms cannot be mixed, labels never
resolve a 3.2 target, and a city is never represented as a metro. The
seller-wide native capability is withheld until its authenticated resolver URL
is deployed.
Two platform shapes are worth knowing before you build a package:
* **Google Ads, Snap, TikTok, LinkedIn, and Spotify** accept top-level
provider-supported ISO 3166-2 regions. Their current region searches do not
return enough ISO hierarchy to prove a deeper subdivision, so Apostra refuses
nested codes rather than risk substituting a larger same-named parent. Meta
has the bounded French translation described above.
Pinterest verifies exact ISO codes; Reddit passes declared ISO-style codes
directly. Those paths do not need this name-based restriction.
* A **Spotify** ad set carries a single country, so a Spotify package declaring
more than one country is refused rather than delivering to just one of them.
Split those countries across packages.
* **Google Ads** targets geography at the campaign level rather than per ad
group, so every package in one Google media buy must declare the same
geography. A buy whose packages disagree is refused with
`INVALID_PACKAGE_MIX` before anything is created. Use separate media buys for
separate geographies.
### Reading geography back in delivery reports
`get_media_buy_delivery` breaks delivery down by geography when you ask for it
through `reporting_dimensions.geo`, and what you get back depends on the
platform:
| Platform | Geo breakdown in delivery |
| - | - |
| Meta | Country or region. Request `geo_level: "region"` and rows come back keyed by ISO 3166-2 code. |
| Google Ads | Country only. A campaign targeted at a region reports against that region's country. |
| Snap, TikTok, Reddit, Pinterest, LinkedIn, Spotify | No geo breakdown. Delivery totals only. |
On Google Ads this means region targeting applies to delivery but is not yet
visible in reporting — you can target a region and cannot yet read that region
back. If you need to reconcile spend against a region on Google, split the
geographies across separate media buys, which reports them separately.
Where geography can be changed, it is **declared state, not an edit**: the
regions you list are the regions that deliver, and a region you stop listing
stops delivering. Sending `geo_regions: []`, or omitting `geo_regions` while
supplying `geo_countries`, returns delivery to country level. Omitting
`targeting_overlay` entirely leaves targeting untouched, so you can change budget
or dates without restating geography.
`get_adcp_capabilities` describes what a platform applies on
`create_media_buy`; it does not yet distinguish the create and update paths.
Until it does, treat the table above as the update contract.
Meta applies `audience_exclude` to Custom and Lookalike Audiences. Other
exclusion dimensions remain platform-dependent and may be refused. Check
`get_adcp_capabilities` for the current list.
## OpenAI Ads
OpenAI Ads connections support delegated OAuth and account-scoped API keys.
Each OAuth grant or key is bound to one ad account, so connect a separate
credential for every OpenAI Ads account you operate. Apostra verifies it with
`GET /ad_account` and refuses a write if the selected account does not match
the account returned by OpenAI.
OpenAI exposes two creative types, and the adapter supports both:
* Sync one public HTTPS image plus a 3–50 character headline, body text of up
to 100 characters, and an HTTPS destination URL.
* Choose impression (CPM), click (CPC), or conversion-optimized (oCPC)
delivery for either creative type. Campaigns launch paused unless the
request explicitly sets `paused: false`.
* Read daily impressions, clicks, spend, CTR, CPC, and CPM from OpenAI's
ad-level insights endpoint, plus attributed conversions for oCPC campaigns.
* Sync a reusable `product_ad_template` containing title, body, and optional
price tokens without uploading an image or destination.
* Create a `product_feed` campaign from an already-linked feed ID, optionally
constrain it with one filter per supported product field, and render the
template using each selected feed item's image and destination.
* Read product-segmented metadata and metrics for feed-backed campaigns.
Every accepted create is read back from OpenAI at the campaign, ad-group, and
ad layers before Apostra reports success. If a child write or readback
fails after the campaign exists, Apostra archives the campaign and reports
whether cleanup was confirmed. Safe reads use bounded retries for rate limits
and server failures; mutation calls are never retried implicitly.
The OpenAI ad account must have `review.status: "approved"` before activation.
The campaign lifetime budget must be at least one major currency unit. The
package `bid_price` is a maximum CPM for impression products, a maximum CPC for
click products, and a CPA optimization input for oCPC products. oCPC still
bills valid clicks. Local-file uploads are not supported.
Country, ISO 3166-2 region, and Nielsen DMA targeting are supported at campaign
creation. Apostra resolves canonical codes against OpenAI's current geo
lookup endpoint and requires the expanded campaign readback to contain exactly
the resolved IDs. For plans already resolved against the provider catalog,
`ext.openai_ads.location_ids` accepts up to 2,500 IDs and cannot be combined
with canonical geography.
The adapter publishes this OpenAI market snapshot at
`products[].ext.openai_ads.market_availability`, verified August 15, 2026:
| Market | ISO code | Available targeting |
| - | - | - |
| Australia | `AU` | Country, region |
| Brazil | `BR` | Country |
| Canada | `CA` | Country, region |
| Japan | `JP` | Country, region |
| Korea | `KR` | Country, region |
| Mexico | `MX` | Country |
| New Zealand | `NZ` | Country, region |
| United Kingdom | `GB` | Country, region |
| United States | `US` | Country, region, Nielsen DMA |
This is a planning snapshot from OpenAI's
[Ads Manager availability](https://help.openai.com/en/articles/20001245-ads-manager-availability)
and [location catalog](https://developers.openai.com/ads/openai-geotargets.csv),
not a substitute for provider authorization. OpenAI's live account-scoped geo
lookup remains authoritative during creation, so newly added markets can work
without waiting for a code release while removed or unavailable locations fail
closed.
Existing provider custom-audience IDs are returned by `get_signals` with their
current provider readiness. They can be included or excluded at create or
update time with
`ext.openai_ads.custom_audience_ids` and
`ext.openai_ads.excluded_custom_audience_ids`; the sets must be unique,
disjoint, present and ready in the selected account, and are verified by exact
campaign readback. Each supplied axis is declarative: it replaces that complete
include or exclude set, an empty array clears it, and omission preserves it.
Optional `custom_audience_bid_multipliers` use OpenAI's provider micros range
and are read back from the ad group. This does not claim the AdCP audience-sync
lifecycle: OpenAI's current API can create or replace an upload and archive it,
but does not expose governed member add/remove operations, so
`sync_audiences` remains disabled. Apostra's guarded provider client accepts
locally SHA-256-hashed email or phone identifiers only; it does not accept raw
CRM identifiers.
For oCPC, put one active standard event setting ID at
`packages[].targeting_overlay.ext.openai_ads.conversion_event_setting_id`.
Before creating anything, Apostra lists the account's event settings and
requires an unarchived, non-custom setting owned by the selected account and
connected to exactly one source. The selected goal is immutable after campaign
creation. `sync_event_sources` discovers and verifies existing event settings;
it does not create pixels, Conversions API secrets, or event settings, and
`log_event` is not exposed. The guarded provider layer can provision those
resources for an operator workflow, but a returned Conversions API key is
one-time secret material and is never persisted or logged by the adapter.
Delivery uses the campaign-scoped insights endpoint, paginates every result,
and reports ad-group totals plus creative rows. Product buys also return
catalog-item rows. Buyers can request country or device-type breakdowns and
hourly, daily, or monthly totals; OpenAI does not allow hourly reports combined
with a product, country, or device segment. Reports count OpenAI's
click-through `conversions` metric at ad-group level. View-through conversions
use OpenAI's separate reporting-only metric and are returned in
`ext.openai_ads`; they are never added to canonical conversions, CPA, bidding,
or billing.
The adapter's AAO community-mirror definition exposes both executable formats
under publisher domain `chatgpt.com`, scoped to the ChatGPT AI-assistant
property and the `sponsored_intelligence` channel. It does not invent a
targetable placement because the Advertiser API does not expose placement
selection.
Optional OpenAI ad-group context hints can be supplied at
`packages[].targeting_overlay.ext.openai_ads.context_hints`. The adapter
canonicalizes them as a unique, sorted set, sends no more than 100 hints, and
requires exact provider readback. These free-form hints can guide contextual
matching, but they do not create or imply a selectable AAO placement.
On update, geography is resolved again against OpenAI's live catalog before
any write. Context hints, bid price, audience multipliers, product-set filters,
and a replacement creative are applied to the selected ad group or ad and read
back exactly. When a live campaign needs a child-resource mutation,
Apostra first pauses the hierarchy, performs and verifies the changes, and
reactivates only after every write succeeds. Activation enables ads and ad
groups before the campaign; pause and archive stop the campaign first so a
partial failure remains contained.
For a product-feed buy, put the already-linked feed ID at
`packages[].targeting_overlay.ext.openai_ads.product_feed_id`. Optional filters
go at
`packages[].targeting_overlay.ext.openai_ads.product_set.filters`. Each filter
contains `field`, `operator`, and non-empty string `values`; fields cannot be
repeated. The comparison operators `gt`, `gte`, `lt`, and `lte` are restricted
to `price` and `star_rating`. OpenAI requires feed connection and initial
catalog upload through Ads Manager and SFTP rather than the public Advertiser
API. Apostra therefore validates and uses an existing feed ID but does not
claim full catalog-management support. OpenAI's account-gated Delta Feeds API
can update the title or availability of existing variants only; acceptance is
asynchronous and is not treated as downstream serving confirmation. The
account-gated Bulk API is also asynchronous and non-transactional. The provider
client requires request- and operation-level idempotency, polls the durable job
ID to a terminal state, validates operation accounting, and returns partial
failure as an explicit reconciliation outcome. Normal media-buy creation
continues to use direct writes with exact readback and containment instead of
claiming bulk atomicity.
An operational contract audit compares the current official OpenAPI with the
required endpoint set and exact creative-type enum. It fails closed if a
required operation disappears or if the provider adds or removes a creative
format before the adapter and AAO definitions are updated.
Use **Connect account** for OAuth when it is enabled for your OpenAI Ads
integration. The account-scoped API-key path remains available as a fallback:
create the key in the OpenAI Ads account, then add an adapter credential with
provider `openai_ads`. Store keys only in the connection flow; do not put them
in prompts, request bodies, logs, or source control. See the [OpenAI Ads API
quickstart](https://developers.openai.com/ads/api-quickstart) for the upstream
account and key prerequisites.
## Meta
Meta is Apostra's most complete direct-adapter integration, spanning the
broadest range of campaign types, creative formats, and targeting options of
any connected platform. This section starts with connecting your account,
then covers execution details for audiences, catalog sales, creatives, and
bidding.
### Connect your Meta account
In Meta Business Suite or Meta Business Manager, add Apostra Business
Portfolio (Business ID: 1091793362240769) as a partner, then grant it access
only to the advertiser assets required for the workflow.
Apostra's validated onboarding path uses an Ad Account owned by a Meta Business
Portfolio. Personal ad accounts can appear during account discovery, but
campaign activation with them has not been validated — use a Business
Portfolio-owned account for production.
Default grant for activation:
* The relevant ad account.
* Campaign management access.
* Performance reporting access.
Additional assets may be needed depending on the campaign:
* Facebook Page or Instagram account access when ads use those identities.
* Facebook Page advertising and lead access when Instant Forms are used.
* Pixel or dataset access when conversion events or conversion optimization are
configured.
* Catalog access when catalog products are used.
Meta's OAuth consent can include `ads_management` and
`business_management`. Page-backed ads also request `pages_show_list`,
`pages_read_engagement`, and `pages_manage_ads`; Instant
Form discovery also requests `leads_retrieval`. Reconnect an existing Meta
connection after these Page or lead permissions are added so Meta
can issue a token with the new grants. The customer-side asset grant should
still stay scoped to the specific business assets needed for the advertiser
workflow. Apostra proves delegated Page and task access before reading the
Page's Instagram relationship with the original User token. Page tokens and the
original User token remain confined to credential handling and are never
returned in adapter results or account metadata.
Apostra does not need full control of the customer's business portfolio, broad
account-settings control, billing administration, or permission to add, edit, or
delete users.
For campaigns targeting European Union locations, also configure both the
default DSA beneficiary and default DSA payor on the selected Meta ad account
before creating the media buy. Apostra reads those account defaults and
sends the exact values on each affected ad set. If either default is missing,
creation stops before any campaign is sent to Meta; Apostra does not infer
or invent the legal identities.
### Agency setup
The client adds the agency's Business Manager as a **Partner** on the ad
account — in Business settings, under Partners, using the agency's Business
Manager ID — and assigns the same scoped access described above (campaign
management plus performance reporting, plus Page, pixel, or catalog assets as
needed). The agency then connects its own Business Manager login in
Apostra; every client ad account the partner grants reach arrives under
the agency's Business Manager on the **Advertiser mapping** tab, ready to map
to the right advertiser.
### Custom and Lookalike Audience targeting
The Meta adapter currently exposes pre-existing Custom and Lookalike Audiences
through `get_signals` but attaches their IDs using the legacy
`audience_include` / `audience_exclude` compatibility path described below.
This is not the final AdCP 3.1.2-native model. Buyer-managed audiences synced
through `sync_audiences` belong in those audience fields; provider-account
signals discovered through `get_signals` should migrate to
`signal_targeting_groups`.
The current Meta compatibility path uses `targeting_overlay.audience_include`
and `targeting_overlay.audience_exclude` arrays on Meta packages. Each value is
the `signal_agent_segment_id` returned by `get_signals` for Meta. The structured
`signal_ref` identifies the signal source; it is not the provider targeting
handle. Inclusion maps to Meta Custom Audiences; exclusion maps to excluded
Custom Audiences. The same fields are returned by `get_media_buys` for both
managed buys and read-only tracked buys.
Before create or update, Apostra reads the selected ad account's complete
audience inventory. Every requested ID must belong to that account and Meta
must mark it usable for delivery. Missing, deleted, owned by a different
account, or non-deliverable IDs are refused before Apostra sends a write to
Meta. After a write, both audience sets must match Meta's readback exactly. If
the readback after creation differs, Apostra removes the new campaign. If
an update differs, Apostra restores the previous targeting and verifies the
restoration with Meta.
On update, each supplied audience field is declarative: it replaces the full
include or exclude list. An empty array clears the list; an omitted field is
preserved. The old Meta-specific `targeting_overlay.signals` extension is no
longer accepted; migrate it to `audience_include`.
### Meta Sales — Website Catalog Sales
The Meta wholesale catalog exposes `meta_sales_website_catalog` for
manual-auction website catalog-sales media buys. A launch needs all of the
following:
* a commerce catalog owned by the selected account's Business Portfolio;
* a non-empty product set that belongs to that catalog;
* an account-authorized Pixel/Dataset with the `PURCHASE` conversion event;
* an authorized Facebook Page, an HTTPS website destination, and a source
creative whose Page and destination match the request.
Catalog and product-set candidates appear in brief execution readiness as
separate, related activation resources. Choosing them writes
`meta_catalog_id` and `meta_product_set_id` into the execution package; they
are not signal IDs and do not belong in audience targeting.
Before any Meta mutation, Apostra rereads the Business Portfolio's complete
commerce-catalog and product-set inventory and rejects missing, empty,
cross-catalog, or foreign selections. It then creates the campaign paused,
binds the exact product set in the ad set's `promoted_object`, materializes a
dynamic product template for that product set, and verifies the Page,
destination, product set, optimization, and measurement readback. The catalog
is preserved as the verified parent of that product set. A mismatch fails
closed and removes the new campaign instead of reporting a successful buy.
### Real-time Meta signals
After you select a Meta ad account, Apostra automatically installs and repairs
its Meta app subscription for processing completion, objects with issues, ad
recommendations, creative fatigue, and product-set issues. No separate Ads
Manager setup is required for each connected account. The connection must keep
campaign-management permission so Meta allows the subscription and subsequent
authoritative readback.
These notifications reduce detection latency; they do not replace provider
readback. Apostra verifies each signed notification and, when it identifies an
ad, ad set, or campaign, reads that object with the current connection before
changing canonical campaign or creative state. Polling remains the fallback if
delivery is delayed or missed, and duplicate deliveries are deduplicated.
`get_media_buys` and `list_creatives` can include recent observations in
the response-level `ext.scope3_provider_signals` object as
`{ version: "1", provider: "meta", signals: [...] }`. Creative-fatigue entries
include the stable `creative_id` when available, the fatigue level when Meta
provides it, and the observation time. Meta emits creative-fatigue notifications
for active ads only. These observations are Apostra extensions, not canonical
AdCP status fields.
### Brief planning in MCP hosts
When Claude, ChatGPT, or another MCP host calls Meta `get_products` with a
non-empty brief, Apostra uses assisted planning by default. The response includes
the interpreted objective, optimization, geography, demographics, creative
format, assumptions, and rationale. If the brief is ambiguous or asks for a
targeting or creative decision that Apostra cannot represent exactly, the call
returns clarification requirements and no actionable product instead of silently
guessing.
Meta brief planning accepts exact inclusive age bounds from 18 through 64. For
example, `ages 21-35` creates product targeting with `age_min: 21` and
`age_max: 35`; familiar ranges such as `25-34` continue to work unchanged.
The numbers must appear explicitly in the brief. Apostra does not use
model-inferred age bounds, and asks for clarification when ranges are below 18,
disjoint, or ambiguous. To control delivery when Meta does not know a person's
age, explicitly say whether to include or exclude people on WhatsApp whose age
is unknown. If an explicit constrained-age brief does not make that choice,
Apostra excludes unknown-age delivery; it never silently enables it. These
demographics travel with the selected product; the AdCP targeting
overlay does not yet expose demographic fields.
Use `buying_mode: "wholesale"` only when the buyer explicitly wants raw Meta
catalogue discovery without brief-strategy provenance. Wholesale discovery does
not claim that Apostra interpreted the advertiser's brief. Tracked campaign
planning uses its separate propose-and-accept flow before an assisted write.
### Meta performance goals and bidding
Meta products declare the performance goals they can execute and accept one
primary goal per package (`max_optimization_goals: 1`). Send that choice in the
standard AdCP `optimization_goals` array. Apostra validates the goal against the
product's Meta objective before creating or updating anything, then reads back
the exact Meta optimization goal, billing event, and bid settings. Media-buy
reads return the effective standard goal rather than the product's default.
When buyers send ranked alternatives, Meta considers explicit priorities first
and executes the lowest-numbered priority. Ties preserve array order, and goals
without a priority follow every explicitly ranked goal. This deterministic
selection matches the upcoming AdCP 3.2 contract while Meta continues to accept
only one primary goal.
In the upcoming AdCP 3.2 contract, bidding is separate from the goal. Apostra's
Meta adapter contains a bounded provider-mapping and readback layer for
automatic bidding, `max_bid` as a hard per-auction ceiling, `cost_per` with
`strength: "cap"`, and `roas` with `strength: "floor"`. The released AdCP 3.1
handlers do not wire or expose that canonical input: requests using `bidding`
fail with a version-gap error instead of being accepted through an untyped
field. A released and negotiated 3.2 SDK must also supply the final typed
handler, pricing-option, inheritance, and value-currency validation before the
canonical surface is enabled. Manual `bid_amount`, target-strength cost/ROAS
policies, and policy combinations remain unsupported because Meta cannot
preserve those semantics exactly.
The deprecated `bid_price` field remains accepted and follows AdCP's
deterministic migration rule. Meta advertises legacy `max_bid: true`, so
`bid_price` normalizes to canonical `bidding.max_bid`; it is not a second
bidding mode. Legacy monetary goal targets normalize conservatively to target
strength and therefore fail closed on Meta rather than being strengthened into
a cap or floor. Canonical bidding is exposed publicly once the released AdCP
SDK carries the 3.2 schema; no private `ext` field is used in the meantime.
Unsupported objective/goal pairs, targets, reach units, and view-duration
thresholds also fail before a Meta write. Check each product's `metric_optimization`,
`vendor_metric_optimization`, and `conversion_tracking` fields for its exact
accepted set.
Vendor-metric goals also require the matching vendor entry in the package's
standard `committed_metrics` array so optimization and delivery reporting use
the same measurement key. Add that append-only contract when creating the
package. AdCP update packages do not currently carry `committed_metrics`, so
Apostra rejects an update that would introduce a different vendor goal instead
of inventing a private field; existing commitments continue to be reconciled
after later goal changes. AdCP `target_frequency` is a soft reach-optimization
band, while Meta's available frequency control is a hard delivery cap; Apostra
rejects the soft goal instead of silently changing its meaning. Use
`targeting_overlay.frequency_cap` only when a hard cap is intended.
### Pixel and dataset ownership
Meta measurement setup is scoped to the selected ad account. Apostra lists the
complete Pixel/Dataset inventory Meta authorizes on that account; if Meta
returns no sources, Apostra reports an empty list instead of substituting the ad
account ID. Installation snippets and Event Match Quality appear only for a
source verified in that inventory. A requested missing or foreign source is
reported as failed. When you request specific source IDs, Apostra returns only
those IDs and does not inspect Event Match Quality for other owned sources.
Before sending a conversion event, an account-scoped connector rechecks that the
Pixel/Dataset is still authorized. If Meta discovery, pagination, or ownership
verification fails, Apostra sends no conversion event. The connector rejects a
request account that differs from its authenticated selected account, and a
pagination continuation cannot change API version, account, or resource. MCP
hosts do not need to add an account field to `log_event`: a complete
`sync_event_sources` call learns private tenant-scoped ownership for the
selected account. A later host-neutral event resolves exactly one fresh learned
mapping, then rechecks the complete Pixel/Dataset inventory with the current
connection before sending. Missing, ambiguous, stale, revoked, or temporarily
unavailable ownership sends nothing and returns a re-sync or retry error instead
of guessing an account.
### Built-in platform conversions
Some ad platforms observe outcomes on their own surfaces, so no buyer Pixel,
SDK, or server event is required. These sources use stable
`:` IDs and appear in `sync_event_sources` only when the
selected account returns affirmative eligibility. They are platform-managed
and discovery-only. Attempts to upsert or delete a built-in source fail for
that source without changing it; `log_event` also rejects built-in sources.
Apostra does not synthesize health status or scores for built-in sources.
Meta can return `meta:onsite` for shop purchases, `meta:messaging` for
Messenger contacts, and `meta:instant_forms` for active Instant Form leads. An
Instant Form is eligible only when the same Meta connection is authorized for
the selected ad account, Meta grants that connection an advertising task on the
owning Page, the Page returns the active Form, and the Page is published. It
does not have to appear in the account's separate promotable-Page list. That
exception applies only to Instant Forms—shop and messaging sources still
require an eligible promotable Page. The source lists only its native event
type. An event optimization goal referencing one of these IDs is accepted only
for a compatible Meta product and eligible Page; Apostra creates the campaign
paused and verifies Meta's exact optimization goal, destination, and
Page-backed `promoted_object` before returning success.
Amazon advertising profiles return `amazon:onsite` for Amazon-observed product
views, add-to-cart events, and purchases. Google Ads accounts return
`google:youtube` for YouTube-hosted actions and `google:onsite` for
Google-hosted actions; each source's event types come from those actions. Other connected
platforms do not silently claim support: TikTok, Snap, Pinterest, Reddit,
LinkedIn, and Spotify do not currently expose built-in event sources through
Apostra; their existing buyer-instrumented sources are unchanged.
### Meta creative format discovery
Meta creative-format discovery honors exact legacy named-format references,
required asset types, minimum width and height, and case-insensitive name
search. These filters compose: every supplied filter must match. A request that
matches nothing returns an empty list instead of the complete Meta catalog.
Paginated discovery returns a bounded page and a signed continuation cursor.
The cursor is short-lived, retry-safe, and scoped to the authenticated tenant,
account context, filters, page size, and current catalog revision. Safe retries
return the same page; changed, expired, or tampered cursors are rejected. Filters that the static Meta
catalog cannot verify—such as publisher/property, accessibility, disclosure,
input/output transformation, responsive, and maximum-dimension filters—return
an error instead of being ignored.
Meta creative sync is scoped to the selected ad account. Image and video
creatives keep the caller's creative ID and declared Meta format across sync,
list, and media-buy operations, while the provider's creative ID remains the
trafficking identity. Submitted image/video assets are returned on list. Review
status comes from explicit provider review evidence on ads; paused, active, or
archived lifecycle status alone is never reported as approval. A creative that
has not been attached to an ad remains `processing`.
Meta also discovers eligible existing posts automatically. A Facebook Page is
the publisher identity; each boostable post from a Page authorized for the
selected ad account appears as a separate read-only creative with a typed
`published_post` asset. Buyers do not copy Page or post IDs into Apostra to make
this inventory appear. Discovery uses the Meta connection's
`pages_show_list` and `pages_read_engagement` permissions, so connections made
before those permissions were requested may need to be reconnected. Page-post
discovery is bounded across the whole account and fails without returning a
partial post inventory if Meta cannot complete it safely.
Existing-post discovery is currently read-only for ordinary buyers. Products
do not yet advertise the existing-post format, and published-post sync or media
buy creation remains restricted to Apostra's signed, paused, no-spend provider
conformance run. Image creative workflows are unchanged. Meta video
availability is described below.
The typed published-post contract, Meta discovery path, and shared host flavor
are shipped. Claude, ChatGPT, and Murph receive the same creative payload and
present any creative with a typed `assets.published_post` asset as **Existing
post**, without changing its creative identity. Uploaded creatives and existing
posts stay together in the same list; a host can request a post-only view
without a Meta-specific picker. No action is needed unless an older Meta
connection reports that Page permissions require reconnection.
Fresh signed paused/no-spend provider evidence remains pending for each enabled
social adapter. A static implementation or runnable canary path is not live
proof, and ordinary buyer availability remains governed by each platform's
compatibility page.
For file-based Meta image formats, the uploaded media and `link_url` are
required. Meta Video Feed, Stories, and Reels are in Testing and are
unavailable through standard buyer sync or assignment. The current Testing path
requires a reachable JPEG or PNG `thumbnail` matching the video's aspect ratio
and at least 1080 pixels wide. The thumbnail may use a portable
`scope3-asset://` reference; the video must use a hosted HTTPS URL. Automatic
thumbnail selection is not available through standard buyer workflows.
`primary_text` is optional on each video format. `headline` applies to
`meta_video_feed`, while `meta_stories` and `meta_reels` accept neither
`headline` nor `description`.
The CTA defaults to `LEARN_MORE` when `call_to_action` is absent. The Page comes
from an explicit authorized `page_id`, or the sole Page authorized for the ad
account; zero or multiple eligible Pages fail before any provider write. Missing
media, destination URL, or resolvable Page also fails closed, so Apostra never
syncs a hollow Meta creative.
Other hosted social adapters also preserve product-scoped canonical format
selections. Google validates the complete required role/count matrix before
building a paused ad or Performance Max asset group, then reads the exact
provider assignment back. Its products expose uploaded Responsive Search,
Responsive Display, video, Performance Max, and Demand Gen formats; generated
display and generated-offering refs fail before provider I/O until a generation
writer exists. Pinterest products expose single-image, video, and max-width
video Pins. Generated, carousel, collection, showcase, and Idea Pin refs fail
before upload rather than being flattened into a single Pin. Pinterest requires
authored Pin title and account/package context before upload and confirms the
paused ad. Spotify products expose authored audio, podcast audio, video, and
in-feed display formats. Generated Spotify refs fail before asset creation until
a generation writer exists. Spotify requires authored advertiser/brand name,
tagline/headline, and a
`call_to_action` text asset selected from the format's advertised CTA values,
plus the declared `click_url` destination.
It applies the same validation in sandbox; live assignment is acknowledged only after
paused-ad readback. Amazon recognizes its canonical catalog selections but returns a
visible unsupported result until its creative writer can create and read back
the provider ad; asset upload or an ASIN alone never counts as creation. Reddit's
current self-serve writer supports canonical freeform promoted posts only, and
LinkedIn supports canonical inline creatives during media-buy creation but
returns a visible unsupported error for post-buy creative sync. Unsupported
format/writer combinations never count as package coverage. Reddit freeform
and LinkedIn inline creatives also require exact paused provider-ad/creative
readback before assignment is returned.
Provider-native creatives that were not synced through Apostra remain visible,
but are returned under their provider ID with the explicit
`meta_unmapped_provider_creative` format. Apostra does not infer an upload format
or reconstruct a source-media URL that Meta does not return. After a synced
creative is updated, assignment, review, and delivery reads include every
retained provider generation while returning only the current source creative.
List cursors are scoped to the account, filter, and snapshot; restart pagination when
the provider inventory or requested delivery snapshot changes.
Apostra discovers the Facebook Pages authorized for the selected ad account. If
there is exactly one, `page_id` may be omitted and Apostra selects it. If there
are several, validation returns bounded `page_id` and Page-name choices without
writing. If more choices remain, repeat the request with the returned
`ext.meta_page_selection_offset` to page through them, then include the chosen
`page_id`. Include `instagram_actor_id` only when the ad should use
that authorized Instagram identity; explicit Page and Instagram IDs are
validated against the selected account before any upload or creative write.
Use the legacy Meta `format_id` returned by `list_creative_formats`. Image and
video inputs must satisfy that format's file type, dimensions, aspect ratio,
duration, size, text, and link requirements. Public HTTPS URLs remain accepted.
The portable image-upload task is available in supported MCP hosts for every
connected sales-adapter Storefront whose private asset store is configured. It
returns a private `scope3-asset://` reference bound to the authenticated buyer,
Storefront, connected credential, and selected adapter account; the signed
upload URL is not exposed to the conversation. Uploading and finalizing that
reference happen within Apostra and do not depend on the destination adapter.
Sending the reference to an ad platform is a separate capability: the adapter
must exchange it for a short-lived delivery URL and confirm exact provider
readback. Meta supports that delivery today. Other adapters reject the
reference before contacting the provider until they add the same delivery
support; their existing supported public-URL creative flows are unchanged.
Apostra validates required roles and request metadata before writing. For public
image URLs it also pins public DNS, rejects redirects, caps downloaded bytes,
and verifies JPEG/PNG content; Meta performs the final provider-side media
processing.
`sync_creatives.assignments` supports one or more `meta_image_feed` creative
and paused-package mappings in one request. Existing-post and video assignments
are in Testing and are unavailable through standard buyer workflows. Apostra
validates every mapping and verifies every target package and campaign belong to
the selected account and are paused before creative writes. It then creates a
paused Meta ad for each mapping, records that provider ad before checking the
package collection, and requires exact provider ad, creative, account, campaign,
package, and paused-status readback. Retries reconcile the recorded ad instead
of creating another one.
For a single package, a fully proven result returns
`ext.manual_action_count: 0` alongside `scope3_meta_assignment`. For multiple
packages assigned to the same creative, `assigned_to` aggregates the provider-
confirmed package IDs and `assignment_errors` reports each failed or rejected
package. The response deliberately omits the singular `ext` evidence block in
that case, because it cannot make one package look like proof for the whole
batch. Treat all requested package IDs appearing in `assigned_to` with no
`assignment_errors` as the successful multi-package outcome. Ambiguity or drift
returns the package in `assignment_errors` and not in `assigned_to`, with no
`ext` block. An ad Meta has rejected in review is a third, differently shaped
case: it satisfies every structural check above, so the package is still listed
in `assigned_to` — but it cannot run without a human fix, so the whole `ext`
block, including `scope3_meta_assignment`, is omitted and a sanitized Meta
review reason is returned in `assignment_errors` under that package id. Review
that is still pending or processing is not a verdict and does not withhold the
claim. Assignment during
`create_media_buy` remains supported too. Sync-time assignment is a separate
post-create operation; the canonical buyer lifecycle does not infer creative
or actor selection from a campaign plan.
Carousel child attachments, native Meta translation/localization, assignment
weights, placement-specific assignments, and creative deletion are not currently
supported. Generated formats without a
provider build-and-readback path are not advertised. Callers should not treat a
successful upload as evidence that native localization ran, that provider
review completed, or that no Ads Manager follow-up is required. Empty
unsupported option collections are accepted as no-ops, but non-empty unsupported
mutations fail before any provider write. Localization hints, including a raw Meta
`asset_feed_spec`, also fail before Page discovery, media upload, or durable
creative reservation instead of being silently removed from an otherwise valid
creative.
Meta's current manual multi-language representation uses an
[`asset_feed_spec`](https://developers.facebook.com/docs/marketing-api/dyn-language-optimization/)
with `optimization_type: "LANGUAGE"`: bodies, titles, descriptions, links, and
media carry labels, `asset_customization_rules` map those labels to Meta locale
keys, and exactly one rule is the default. Supported keys come from Meta's
`/search?type=adlocale` response. Apostra does not pass this provider object
through until the request has a typed AdCP representation and every requested
locale, copy/media asset, Page/Instagram actor, provider creative, and provider
variant label can be read back exactly. Dry runs read durable idempotency state
and provider readback where needed, and perform no provider or ledger mutation.
### Supported buying types
Apostra creates all Meta campaigns using the **Auction** buying type.
Reservation (Reach & Frequency) buying type is not supported.
### Reading campaign budgets and daily budgets
Meta can hold a campaign's budget in three places: on each ad set as a
lifetime budget, on each ad set as a daily budget, or once on the campaign
(Advantage campaign budget, which Meta shares across the campaign's ad sets).
AdCP 3.1 has only a lifetime `total_budget` and a per-package `budget`, so
`get_media_buys` reports each shape as follows:
* **Every ad set has its own lifetime budget.** `total_budget` is the sum of
the ad set budgets and each package carries its `budget`. Budget updates are
offered (`update_budget`). This is the shape Apostra creates by default.
Meta can report an unused budget field as `0`: an ad set whose lifetime
budget is `0` is read as having no lifetime budget when the campaign holds
the budget or the ad set has a daily budget.
* **Campaign budget, lifetime.** `total_budget` is the campaign's lifetime
budget. Packages carry no `budget`, because Meta decides each ad set's share.
This also applies to a campaign-budget campaign that has no ad sets yet.
* **Campaign budget, daily.** `total_budget` is `0`, because the campaign has
no lifetime budget. The daily amount is in `ext.scope3_meta_budget`. Apostra
does not multiply it into an invented lifetime amount.
* **Daily ad-set budgets.** `total_budget` counts only ad sets that have a
lifetime budget (`0` if none do). Each daily ad set carries no `budget` and
states its daily amount in the package's `ext.scope3_meta_budget`.
For every shape except the first, `update_budget` is not offered and a budget
update is refused, because a per-package lifetime budget change cannot be
applied. Per-package `bid_price` and `optimization_goals` changes are also
refused on a campaign-budget buy, because Meta shares one bid strategy and
optimization across its ad sets; no change is made on Meta. To change these,
create a new media buy.
Media-buy `ext.scope3_meta_budget` is present only for the three
non-default shapes:
| Field | Meaning |
| - | - |
| `budget_scope` | `campaign` when Meta holds the budget on the campaign; `ad_set` when it is on the ad sets. |
| `budget_period` | `lifetime` or `daily` for a campaign budget; `daily` when every ad set is daily, or `mixed` when some ad sets are lifetime and some daily. |
| `daily_budget` | The campaign's daily budget in `currency`, for a daily campaign budget. |
| `total_budget_basis` | What `total_budget` covers: `campaign_lifetime_budget`, `ad_set_lifetime_budgets` (daily ad sets excluded), or `none` (no lifetime budget exists, so `total_budget` is `0`). |
Package `ext.scope3_meta_budget` is present on a package that carries no
`budget`:
| Field | Meaning |
| - | - |
| `budget_scope` | `campaign` for an ad set under a campaign budget; `ad_set` for an ad set with its own daily budget. |
| `budget_period` | `daily`, for an ad set with its own daily budget. |
| `daily_budget` | That ad set's daily budget in `currency`. |
| `lifetime_spend_cap` | Under a campaign budget, the most this ad set may spend over its lifetime, when one is set. |
| `lifetime_min_spend_target` | Under a campaign budget, the least this ad set should spend over its lifetime, when one is set. |
Daily per-ad-set spend limits under a daily campaign budget are not reported.
When Apostra has a recorded bidding policy set once for the whole media buy,
AdCP 3.1 has no field for it, and repeating it as each package's `bid_price`
would misstate where it applies. The recorded policy is reported once as
`ext.scope3_meta_bidding: { "scope": "media_buy", "bidding": { ... } }`, and
packages carry no `bid_price`. A provider-authored campaign-budget buy whose
shared bid policy was not recorded also carries no package `bid_price`; Apostra
does not recast that shared policy as a package setting.
A read still fails when Meta reports a combination that cannot be stated
truthfully: a campaign budget together with ad-set budgets, both a lifetime
and a daily campaign budget, an ad set with no budget at all, or a bid
setting that differs from the buy's recorded bidding policy. Other existing
checks, such as unsupported optimization goals or bidding, also still fail
the read. When one campaign on an ad account fails, a read of the whole
account fails. A campaign that reports both a campaign budget and ad-set
budgets can still be paused or canceled; any other update to it is refused
before Meta is changed.
### Meta app-install readiness
For Meta app-install campaigns, plan the campaign around mobile-first creative
and privacy-safe app-event measurement before scaling spend:
* Use Advantage+ App Campaigns as the default starting structure unless the
advertiser has a specific manual-buying reason.
* Start with lowest-cost bidding while the campaign is learning; move to cost
controls only after the account has stable cost and conversion history.
* Keep placements open across Feed, Stories, and Reels so Meta can allocate
delivery, and design Reels-ready vertical creative rather than treating Reels
as an afterthought.
* Launch with at least 5-10 distinct creative variants. Prefer short vertical
video that shows the app experience quickly, then keep refreshing variants as
the campaign learns.
* For iOS, confirm SKAdNetwork or MMP postbacks and Conversions API event
forwarding before scaling. Evaluate D7/D30 cohort ROAS instead of day-1
last-click results.
* Once trial, purchase, subscription, or other downstream app events are flowing
reliably, optimize toward those events rather than raw installs.
## TikTok
### Connect your TikTok account
Ask the customer to share the advertiser account from TikTok Business Center to
Apostra's Business Center as a partner.
Default grant for activation:
* Advertiser account role: `Operator`.
Use `Analyst` only for reporting-only access. `Analyst` can view ads,
performance, and reports, but it cannot create or edit ads or manage audiences.
Use `Admin` only when there is an explicitly approved reason to manage ad
account settings. Admin is not the default activation role.
This role is a TikTok-side permission, chosen when the customer shares the
account — Apostra does not read or enforce which role was granted, so choose
deliberately at share time.
TikTok [fixes an ad account's currency when the account is created](https://ads.tiktok.com/help/article/account-setup-faq?lang=en\&redirected=2).
Apostra reads that currency directly from the selected advertiser for product pricing,
campaign creation and budget or bid updates, account financials, media-buy inventory, and delivery. If
TikTok returns another advertiser, omits a requested account, reports an
unknown account status, or returns malformed balance, timezone, identity, or
currency data that conflicts with the selected account or accepted product,
Apostra stops before a campaign budget or bid mutation. It never substitutes USD
or combines money from different advertiser currencies. TikTok's approved status is
shown as active, its closed status is shown as closed, and its recognized review,
verification, punishment, audit, authorization, and contract states are shown as
suspended. Unknown or missing status and role values fail closed.
Account discovery returns the complete accessible TikTok account inventory
when pagination is omitted. Clients that request pages receive accounts in a
stable order and must follow the opaque continuation cursor until `has_more`
is false. Continuations are valid only for the same connection, filters, and
provider snapshot; expired, changed, or modified cursors fail and must be
restarted. Status filters are supported. TikTok does not identify these OAuth
advertiser grants as sandbox accounts, so `sandbox: true` returns no production
accounts.
Additional assets may be needed depending on the campaign:
* TikTok account delivery access for Spark Ads.
* Use "Only show as ads" when Apostra should be able to run Spark Ads without
publishing posts to the TikTok profile.
* Use "Show on TikTok profile and as ads" only when organic profile publishing
is explicitly intended.
Apostra does not need Business Center Admin, finance roles, or permission to add,
edit, or delete users.
### Agency setup
The client shares each advertiser account from their TikTok Business Center to
the agency's Business Center as a partner, using the agency's Business Center
ID, and assigns the `Operator` role (the same role guidance as above). The
agency connects its own Business Center login in Apostra; the shared
advertiser accounts appear grouped under the agency's Business Center on the
**Advertiser mapping** tab — map each to the right advertiser.
### TikTok catalog sync
Apostra syncs a catalog only after TikTok confirms that the selected
advertiser owns it and returns the complete catalog and feed inventory. The sync
stops without changing the TikTok catalog if TikTok returns inventory from
another advertiser, omits a page, or returns duplicate or malformed catalog or
feed records. Apostra does not fall back to another advertiser or treat a
partial inventory as complete.
If a catalog that previously synced now fails, confirm that the connected
TikTok advertiser owns the catalog and that TikTok can return its complete feed
inventory, then retry. This buyer-side check does not change seller storefront
setup, ranking, inventory labels, or any other buyer-visible seller information.
Every TikTok campaign package must include a supported ISO country, either in
the package targeting or its offering geography. Apostra stops before
creating a campaign when a country cannot be mapped or when the request includes
regions, unresolved interest themes, or another targeting dimension that TikTok
cannot yet apply and read back exactly. It does not silently broaden delivery.
For supported manual-campaign targeting, Apostra explicitly disables
TikTok's smart audience and interest expansion. Smart+ products instead use
TikTok's native automated campaign hierarchy and automatic placement. In both
paths, Apostra reads the new paused hierarchy back before activating
delivery. If TikTok returns different country, age, gender, audience,
optimization, event-source, identity, destination, or creative settings—or the
readback belongs to another advertiser—the campaign is not reported as
successfully created. Apostra reports the failure and requests deletion of
the new campaign.
After that paused readback, Apostra records the exact requested targeting,
TikTok confirmation, concrete product, advertiser, and provider ad-group
identity in tenant-isolated durable state. Subsequent campaign reads return the
provider-neutral requested and current targeting plus an exact-match result. An
Apostra-created campaign fails closed if a provider-returned ad group loses
that provenance or its targeting changes unexpectedly. TikTok-native campaigns
remain readable without an Apostra request claim, and stale provenance is
ignored only for ad groups TikTok has authoritatively removed. Terminal campaign
deletion also removes the durable targeting record.
### TikTok creative sync and Spark ads
TikTok creative sync keeps your original creative ID and TikTok format across
adapter restarts. Apostra stores uploaded media and Spark published-post
references under the selected advertiser, then uses the provider media or post
ID as the typed TikTok trafficking reference. Mapped list rows return the normalized submitted image
or video URL. Retrying the same upload reuses provider media after readback; an
upload whose outcome is unknown is not repeated automatically. Image success
always requires an exact, displayable readback from the selected advertiser's
media library.
For video, Apostra records the TikTok video ID before checking processing status,
so a temporary missing-status response resumes against the same video instead
of uploading another copy. The same exact-ID recovery applies if the adapter
restarts between its final durable journal steps. Structured TikTok format
references must use the format authority returned by `list_creative_formats`.
Image and video URLs must include a recognized file
extension that matches both the supplied asset type and selected TikTok format;
extensionless, audio, and mismatched media are rejected before upload. Changed
images use a bounded source-digest filename to satisfy TikTok's per-account
filename uniqueness requirement.
Product-selected TikTok uploads use the URL-free `format_kind` and
`format_option_ref` returned by the product. Uploaded image, video, and carousel
formats require authored `display_name` and `ad_text` (12-100 characters);
Apostra stores that copy with the durable media identity and uses it when the ad
is created. Missing native copy fails before trafficking—Apostra does not insert
placeholder brand names or sponsored-content text.
Post-buy TikTok package attachment is currently rejected before media upload;
registering media without creating and reading back an ad is never reported as
package coverage.
TikTok carousel is available for Reach, Traffic, Website Conversions, and App
Promotion products. Supply one `tiktok_carousel` creative with 2-35 distinct
square image assets in the advertised order. Apostra uploads every image to
the selected advertiser and waits for TikTok to mark each exact image both
displayable and carousel-usable. It then creates one paused `CAROUSEL` ad and
requires TikTok to return the same complete ordered image list. Missing,
duplicate, reordered, substituted, or ineligible images stop the campaign before
activation. `list_creatives` returns the original carousel as one source
creative instead of exposing its uploaded children as separate duplicates.
TikTok's Marketing API does not expose a provider-native creative
translation/localization operation. Apostra therefore does not advertise
native creative localization and does not infer it from language targeting.
`list_creatives` returns the original ID for Apostra-synced creatives and keeps
unmapped provider-native images/videos visible under their provider IDs. It
exhausts uploaded media, assigned ads, performance pages, and advertiser-authorized
`AUTH_CODE` TikTok VIDEO rows before claiming a complete result. A missing
connection identity, a cross-account or inactive mapping, stale provider media,
or malformed, expired, or unauthorized Spark reference stops the operation
before campaign creation. Only explicit, provider-authorized AUTH\_CODE rows are
accepted; missing identity types, legacy identity types, carousel rows, revoked
authorization, and missing provider authorization start times fail closed. The stored Spark
post and advertiser-scoped identity must match the active provider generation.
Uploaded media must read back as displayable before campaign creation. Channel
accounts use a deterministic key over the advertiser and exact AUTH\_CODE identity,
so distinct grants remain isolated. A completed older creative generation cannot
report unchanged or become active after a newer generation exists. Its historical
assignment, review, and performance evidence remains joined to the active source
creative. Each newly created TikTok ad remains paused while Apostra requests its
explicit creative fields. The exact uploaded media ID and format, or the exact
Spark post ID and AUTH\_CODE identity, must read back before activation; a missing
or substituted creative triggers campaign cleanup. Media-library
displayability and Spark authorization do not prove ad review: an unassigned
creative remains `processing`. Explicit assigned-ad readback supplies approval
or rejection status plus package and redacted provider-status evidence. Approval
requires every assignment to be explicitly delivering or completed. Audit
denials, partial approval, Promote audit failure, and terminal transcode failure
are rejected; paused, pending, partial-review, budget, schedule, account, and
asset-lifecycle states remain processing. Unknown provider statuses fail closed.
Assignment inventory is mandatory; any incomplete page or transport failure fails the read.
Apostra verifies that each assignment and review-evidence row belongs to the
selected TikTok advertiser; foreign or malformed evidence fails the read rather
than changing a creative's assignment or review state.
TikTok's AUTH\_CODE endpoints do not return post publication time. A Spark row
synced through Apostra uses its durable registration timestamps. A provider-native
authorized row uses TikTok's UTC authorization start as its creative-availability
timestamp and labels that basis in extension evidence; authorization timestamps
are not presented as post creation or modification time. TikTok timestamps must
use the documented UTC form or explicitly zoned RFC3339; numeric epochs,
date-only values, offsetless ISO strings, and invalid calendar dates are rejected.
Existing TikTok connections
must remain connected through Settings > Connections and grant TikTok Posts
Management query access so Apostra can verify AUTH\_CODE authorization before writes.
## Google Ads
Ask the customer to grant Apostra manager-account or account-level access to the
Google Ads account that will run the campaign.
Default grant for activation:
* Campaign management access.
* Reporting access.
Additional access may be needed depending on the workflow:
* Customer data or data-manager permissions when first-party audiences,
enhanced conversions, or similar measurement workflows are enabled.
* Conversion action access when Apostra is expected to configure or send
conversion events.
Apostra does not need Google Ads account ownership, billing administration, or
permission to add, edit, or delete users.
For a mapped advertiser account, campaign inventory includes active, paused,
and removed campaigns plus their ad groups (or Performance Max asset groups).
Apostra rejects the inventory read if Google returns an incomplete or
cross-account hierarchy; it does not silently present a partial account mirror.
For Search, Display, Performance Max, and Demand Gen campaigns, every returned
revision is derived from the complete provider campaign and child state.
Updates reject stale revisions before writing, then read back pause/resume,
seller-package budget, supported ad-group bid, and advertiser-local schedule
changes from Google. If one part of a multi-entity update fails, Apostra restores
the prior state or verifies that the campaign is paused before returning the
error. Cancellation pauses an active campaign first and is not reported as
complete until Google returns the removed campaign in terminal history.
Reporting-only Google campaign types return no write actions.
### Search targeting
The currently advertised Search Traffic and Search Conversions products require
exact country or region targeting and at least one positive keyword. Supply
keywords on each Search package when creating the media buy. This is a package
object inside the request's `packages` array:
```json theme={null}
{
"product_id": "google_search_traffic",
"budget": 1000,
"pricing_option_id": "",
"targeting_overlay": {
"geo_countries": ["US"],
"keyword_targets": [
{ "keyword": "running shoes", "match_type": "exact" },
{ "keyword": "trail shoes", "match_type": "phrase" }
],
"negative_keywords": [
{ "keyword": "free", "match_type": "broad" }
]
}
}
```
`broad`, `phrase`, and `exact` match types are supported. Per-keyword bids,
duplicate semantic targets, a keyword present in both the positive and negative
sets, keywords longer than Google's limits, and any unsupported targeting field
fail before a Google campaign or budget is written. Creative headlines and
descriptions are never used as implicit keyword targets.
The seller-wide AdCP keyword capability remains withheld because that
capability also promises mutable keyword add/remove operations in
`update_media_buy`. Google keyword targeting is currently supported only by
the two bounded Search creation products above; attempts to replace keywords
on an existing buy fail before provider writes.
An exact age range, gender, or interest written in the discovery brief is stored
with the selected product and applied at the ad group. Age must be an exact
union of Google's closed `18-24` through `55-64` buckets; Apostra does not widen
a range or treat the open-ended `65+` bucket as an exact closed maximum. An
interest must resolve to one provider taxonomy entry in the mapped advertiser
account and advertise Search plus all locales in its provider availability.
Geography uses Google's presence-only delivery setting rather than
the wider presence-or-interest default. Audience criteria use targeting mode,
not Observation mode.
Creation remains paused while Apostra reads back campaign geography and every
ad-group demographic, interest, audience, positive-keyword, and
negative-keyword criterion. The durable readback is scoped to the tenant,
mapped Google customer, campaign, and package. A later `get_media_buys` call
returns `packages[].ext.scope3_requested_targeting`,
`packages[].ext.scope3_confirmed_provider_targeting`, and
`packages[].ext.scope3_targeting_matches_request: true` only when the live
provider state is still an exact set match. Missing, extra, inactive, ambiguous, or
foreign-account criteria make the read incomplete.
Display, Demand Gen, and Performance Max product contracts remain blocked for
creation. Their targeting is not silently routed through Search criteria;
requests fail before provider reads or writes.
### Customer Match audience targeting
New creation accepts `targeting_overlay.audience_include` and
`targeting_overlay.audience_exclude` only on the two advertised Search
products. The governed update path can also reconcile audiences on an existing
Display buy, but Display product creation remains blocked. Pass the canonical
`signal_agent_segment_id` returned by `get_signals` after syncing the Customer
Match audience; never pass a Google user-list resource name or numeric user-list
ID. Apostra resolves each buyer ID through the mapped customer account and
refuses missing, pending, deleted, foreign-account, or non-deliverable lists
before creating provider objects.
#### Retry Google Ads audience syncs safely
S2S integrations can reuse the same `sync_audiences.idempotency_key` and
identical request for 24 hours. Apostra returns the first saved response without
sending another Google Ads write.
Treat these conflict codes as a request to inspect the audience in Google Ads
before retrying:
* `AUDIENCE_IDEMPOTENCY_KEY_REUSE`: the same key was used with different
request content.
* `AUDIENCE_IDEMPOTENCY_KEY_EXPIRED`: the key is outside the 24-hour replay
window.
* `AUDIENCE_REQUEST_OUTCOME_UNCERTAIN`: Google Ads may have received the
request, but Apostra could not verify the outcome. This code can accompany
HTTP 503 on the original request or HTTP 409 on its retry.
Seller MCP responses preserve that direct-adapter status as
`adcp_error.details.http_status`; MCP transport success does not indicate that
the tool call succeeded.
After inspection, use a new key only when another write is safe. Chat, UI, and
other platform-managed buyer workflows require no changes. This retry behavior
does not change seller storefront ranking, inventory presentation, or setup.
The audience arrays are declarative on update: a supplied array replaces that
include or exclude set, an empty array clears it, and an omitted field preserves
it. Performance Max audience signals can expand beyond the named audience, and
Demand Gen uses a different grouped-audience resource, so neither campaign type
claims this exact include/exclude contract.
Campaign readback returns canonical buyer IDs in
`packages[].ext.scope3_confirmed_provider_targeting.audience_include` and
`packages[].ext.scope3_confirmed_provider_targeting.audience_exclude`, alongside
the same IDs in `packages[].ext.scope3_requested_targeting`. Native Google
user-list IDs are never returned.
`packages[].ext.scope3_targeting_matches_request: true` means the complete include and exclude
sets were confirmed against Google; a mismatch fails the write and triggers
containment or rollback.
### Agency setup
Google's flow runs in the other direction: the agency sends a link request
from its **manager account (MCC)** using the client's ten-digit customer ID,
and the client accepts it from their account's access settings. The agency
then connects the manager account login in Apostra; client accounts under
the MCC are discovered through the manager grant and appear under it on the
**Advertiser mapping** tab — the MCC itself is context only and can never run
buys.
## Snap
### Connect your Snap account
Ask the customer to connect, through OAuth, a Snap user who already has an
ads-management role on the relevant Organization or ad account. Snap access is
user-bound: Apostra discovers whatever ad accounts that connecting user's
login can reach. A customer only needs to invite a separate user first when the
person completing the connection isn't already a member — see **Agency setup**
below for that case.
Default grant for activation:
* Ads management access.
* Reporting access.
Additional access may be needed for profile-backed creative workflows or
measurement. New and reconnected hosted Snap connections request both
`snapchat-marketing-api` and `snapchat-offline-conversions-api`. Existing
connections must reconnect in **Settings > Connections** before conversion
measurement is available. `snapchat-profile-api` remains excluded unless Snap
has allowlisted Apostra app for Public Profile API access; profile-dependent
creative workflows remain unavailable until then.
For Snap server-side website and offline conversions, select the ad account that
owns the Pixel. Apostra reads every page of the account's Pixel inventory,
rejects malformed, duplicate, or cross-account rows, and lists every Pixel Snap
authorizes. If the account has multiple Pixels, Pixel-optimized packages must
select one `event_source_id`; conflicting selections, a missing Pixel, or a
foreign ID stop before campaign creation. Setup code appears only when Snap
returns a provider-issued snippet. The base Pixel inventory proves configuration
and ownership, not signal quality.
Before conversion delivery, Apostra rechecks Pixel ownership, preserves
event IDs for deduplication, and forwards a supplied test-event code. Snap's
documented exact `VALID` receipt confirms successful event processing, so the
confirmed batch count is reported as both received and processed. Matching,
attribution, and reporting finalization remain separate and asynchronous. An
HTTP success without the exact `VALID` receipt is treated as a failed batch.
Provider response bodies and access tokens are not exposed in diagnostics.
Mobile-app conversion delivery remains unavailable until Apostra can verify
the Snap App ID and required app metadata.
Apostra does not need organization administration, billing administration, or
permission to add, edit, or delete users.
### Agency setup
Snap access is member-based: each client invites the agency's user into their
Snap Organization as a member with an ad-account role that can manage
campaigns and read reporting. The Snap grant is tied to the authorizing user,
so the agency connects that user's own Snap login in Apostra — the
connection discovers every ad account the login can reach across all client
Organizations, grouped by Organization on the **Advertiser mapping** tab.
For brief-led discovery in Apostra, include one clear objective, exactly
one supported country, and at most one gender. Snap language targeting is not
available for new product discovery in this release. Requests that include
AdCP 3.2 `targeting_overlay.language`, or request mutable package-time language
through `required_overlay_support.language`, return no Snap products and an
actionable `unsupported_language_targeting` clarification before forecasts,
product persistence, or provider writes. Returned Snap products do not
advertise language overlay support or accepted language values. A future
language-bound release can document the exact supported language values once
that buying path is enabled.
Apostra also returns no actionable product when a Snap brief lacks a clear objective or country,
contains competing or negated objectives, requests targeting exclusions or
unresolved interests, includes prompt-injection language, or asks for an
unverified localization, age, or creative capability.
For selected accounts, clear briefs receive a reviewable strategy with
Snap's exact objective, optimization goal, targeting intent, and supported
image or video format. After the deterministic safety check, Apostra reads
the exact selected account and uses its currency for returned and stored
pricing, then verifies the requested country against Snap's complete cached
read-only targeting inventory before account-history ranking or product
persistence. If Snap reports that inventory unavailable or returns no countries,
product discovery returns a transient `SERVICE_UNAVAILABLE` error with a fixed
safe message. If an HTTP-success response has a malformed inventory schema, the
same operation returns a terminal response-contract error and asks the buyer to
contact Apostra support rather than retry indefinitely. Both failures include a
Apostra-generated UUID correlation ID that support can match to logs. Briefs that
need clarification make no provider request. Both clear briefs and an empty
“return the complete available Snap catalog” request require a selected account.
If Snap denies discovery access, Apostra returns a terminal Snap permission
error with guidance to reconnect the account's OAuth access or resolve a Snap
business-review or account restriction before retrying.
Snap language targeting is not available for new product discovery in this
release. If discovery includes `targeting_overlay.language` or requires mutable
language overlay support, Apostra returns `unsupported_language_targeting`
before forecasting, saving products, or writing to Snap. Discover Snap products
without language targeting and book only products returned by that discovery
response. A follow-up release can add language-bound Snap products after the
server-side validation is deployed everywhere. A package that already contains
an `AD_TO_CALL` or `AD_TO_MESSAGE` ad cannot change its country targeting;
Apostra returns a recoverable error before mutation.
USD-authored advisory floors
and static budget guidance are not relabeled: non-USD products use a zero floor
and omit unsupported static guidance. Before campaign creation, Apostra
rechecks the provider currency against any trusted account snapshot, declared
media-buy currency, accepted product, and assisted-execution snapshot, and
stops before a write when they disagree. Every campaign, package, offering, and
bid amount must also convert exactly to Snap's integer microcurrency within the
safe range before creation. When an offering omits its own budget, Apostra
splits the package's integer micros in request order and gives any remainder
micros to earlier offerings, preserving the exact total across retries. Live non-spend evidence is still
required for each non-USD account used in production readiness review.
An assisted Snap create accepts exactly the concrete product, pricing option,
objective, optimization goal, country, age, gender, and pricing snapshot that
was proposed and accepted. A changed or incomplete snapshot stops before a Snap
write. The accepted package intentionally contains no inline creative,
offering, or catalog input: Apostra creates it paused, then performs
creative assignment through a separate fail-closed sync while it remains
paused. After creating each ad squad, Apostra reads its targeting back from
Snap and requires an exact normalized match before continuing. It records the
requested targeting, Snap-confirmed targeting, concrete product, connection,
advertiser account, campaign, and ad-squad identity together; if that durable
record cannot be written, Apostra deletes the paused campaign instead of
returning success; deletion is reported complete only after Snap's terminal
readback confirms it.
Campaign reads use that durable record to populate each package's `productIds`,
`requestedTargeting`, `providerTargeting`, and `targetingMatchesRequest` fields.
Only country, region, metro, postal, language, age, and gender values cross
this API boundary. Interest and audience identifiers remain private, but
changes to them still make `targetingMatchesRequest` false because equality is
checked on the complete normalized Snap targeting. A durable record is visible
only to its exact connection and advertiser account. An incomplete matching
record fails the read instead of inventing product or targeting provenance.
Snap creative discovery currently returns only full-screen 9:16 image and
video formats. Both require a brand name and a headline of at most 34
characters. Images must be JPEG or PNG, exactly 1080x1920, and at most 5 MB.
Direct-upload video must be MP4 or MOV, exactly 1080x1920, 3-180 seconds, and
at most 32 MB. During `sync_creatives`, Apostra validates every downloaded
file before creating Snap media or creatives. Media URLs must use HTTPS on
port 443; redirects, private or transition network addresses, mixed
public/private DNS results, oversized bodies, and mismatched file types fail
before those Snap writes.
Apostra keeps a tenant- and advertiser-scoped creative identity. Retrying
the same buyer creative with the same assets returns its existing provider
creative instead of creating another one. A changed creative becomes the new
active generation while the prior provider identity remains retained as
history, and creative listing continues to use the stable buyer creative ID
with the original name, format, and assets you submitted.
When `sync_creatives` assigns that source creative to a package, Apostra
first confirms that the campaign and Ad Squad still belong to the selected
Snap account and reads the buyer's durable paused intent. It journals the
assignment before creating the Snap Ad, so
retrying after a lost response reuses the same exact provider Ad instead of
creating another one. Ordinary `get_media_buys` reports Snap review state,
provider creative identity, requested paused intent, provider campaign/package
pause state, whether lifecycle intent is available, and whether those states
match. If the intent store is unavailable, provider state remains readable but
buyer intent and the match result are explicitly unknown; Apostra does not
infer them from Snap. Exact source/media/creative/Ad materialization is available
only to Apostra's protected staging readiness check. That check also
requires a future-dated paused campaign, a paused Ad, and zero delivery before
it records readiness.
Story, Collection, Lead Generation, Sponsored Snap, Commercial, generated, and
promoted-offering formats are not currently advertised; stale selections fail
before provider or media-source access.
This discovery change does not edit, pause, or delete existing campaigns or
provider creatives. They can continue delivering in Snap. If a connected
account contains a creative in one of the unadvertised formats, Apostra
leaves it in Snap and returns `UNSUPPORTED_CREATIVE_FORMAT` instead of showing
an incomplete creative inventory.
If Snap is still processing any creative media, creative inventory stays
fail-closed and returns `CREATIVE_MEDIA_NOT_READY`. Retry after Snap finishes
processing the media.
Buyer-paused Snap campaigns stay paused after delayed creative assignment in
both ordinary and protected paths. Only an explicit resume can change that
intent, and resume is rejected until every live Ad Squad has a creative.
Resume completes creative replacement and readback while paused, then activates
each Ad, each Ad Squad, and the Campaign in that order. The explicit resume is
recorded durably after preflight so a process restart can finish it safely. A
failed or ambiguous resume is contained back to paused state and restores paused
intent before the error returns.
Campaigns created before durable lifecycle intent was introduced require one
explicit pause or resume before another budget, schedule, or creative update;
Apostra does not infer buyer intent from provider state.
Before creating any ad squad, Apostra requires the new campaign to return
and read back under the selected advertiser account as paused and not deleted.
Each new ad squad must likewise return and read back under that campaign as
paused and not deleted, including for paused requests. A campaign identity that
belongs to another advertiser account is never paused or deleted; the request
fails with an ownership-isolation error for operator review.
Snap sometimes communicates a rejected operation inside an HTTP success
response. Apostra accepts a media-buy or control-plane single-entity read or
write only when both Snap's request and its one entity result report success and
the returned identity and parent scope match the requested object. Snap's
campaign and ad deletions use the provider's documented empty acknowledgement,
followed by a required terminal readback. Otherwise Apostra stops before the
next lifecycle step and returns a fixed error category with an Apostra-generated
correlation ID. Snap request IDs, messages, debug text, and identifiers are not
returned or logged.
Public Profile selection follows every canonical Snap Business API page and
stops if the complete inventory does not identify exactly one usable profile.
Before mutating a global campaign, ad-squad, or ad ID, Apostra reads its
parent chain back to the selected advertiser account. Creative assignments
preflight every target package through the same chain before media or creative
writes. Media upload must acknowledge the allocated media ID before polling, and
bid estimates must match the requested optimization goal with a valid numeric
range before use. Native catalog creation derives its organization from the
selected Snap account rather than request context, requires that account's
currency, and maps the catalog to a supported Snap vertical. Apostra stores
complete native catalog inventory as a durable pre-write baseline before
creating anything. Selected-account and currency checks complete before the
create is reserved, so a failure before the POST can be corrected and retried.
A first sync never adopts a matching catalog created manually
in Snap. If the create response is lost or ambiguous, retries remain read-only
and may recover only one matching catalog that was absent from that baseline;
they never send another create request. Missing or multiple new matches stop
safely for a later retry. The durable boundary requires a stable connection
credential or principal, uses the host credential key or client identity rather
than optional metadata, and never uses the rotating OAuth access token or a
non-durable direct-call fallback. Catalog
data and delete-missing changes remain staged until provider checks and conflict
arbitration succeed. The final account mapping update is atomic and revision
checked, so a stale concurrent delete-missing request changes nothing. Retrying
reuses a completed Snap create rather than posting another catalog. The complete
source catalog participates in conflict detection, so concurrent configurations cannot overwrite the winning mapping.
Stored offering and job catalog references must be non-empty, well formed, and
belong to the same connection and selected account before Snap receives a
request. Catalog data, Snap identities, discovery, and delete-missing behavior remain isolated
when connections share an account or catalog ID. A Snap identity is reused only
from that connection's completed journal and must still match the selected
organization, name, and catalog type. Feed creation revalidates the catalog's organization
against that account and sends Snap's required catalog, currency, URL, and
schedule fields. Apostra retains the Snap catalog identity before feed setup,
so retrying after a rejected or lost feed response reuses the same catalog. An
already-created feed is reused only when its complete paginated readback exactly
matches the effective requested name, currency, URL, and interval. Snap may omit
documented primary, active, UTC, interval-count-one, or minute-zero defaults from
a response; Apostra normalizes those defaults before comparison. Explicit
drift or ambiguous inventory stops without another provider write. Feed creation
has its own connection/account/catalog-scoped durable journal. If an accepted
feed is not yet visible after a lost response, every retry stays read-only until
the exact feed appears. When `catalog_ids` selects only part of a request,
unselected catalogs create no Snap object, feed, journal, or local mapping.
If a later Ads or Business API inventory page fails at the HTTP, provider, or
sub-request layer, Apostra returns an incomplete-inventory flag and
correlation ID instead of returning the successful prefix. The legacy endpoint
uses `INCOMPLETE_PAGINATION`; the standard SDK endpoint uses
`SERVICE_UNAVAILABLE`. A short enum-shaped provider code may be included;
provider text and identifiers are withheld. These guarantees apply to the
campaign, ad-squad, ad, creative, media-upload, bid-estimate, Public Profile,
Pixel, and catalog client methods covered here. Audience batch/poll and
Conversions API HTTP-success provider-envelope handling remain separately
tracked work; this is not a universal claim for every Snap endpoint.
For an unpaused request, Apostra activates and verifies each ad squad while
the campaign remains paused, then activates and verifies the campaign last. A
fresh provider read must still match the expected parent campaign or advertiser
account immediately before each activation PATCH; ownership drift or a deleted
row sends no PATCH. Provider identity and status must match at every readback, and deleted
tombstones never satisfy a live status. For any post-campaign-create failure
after advertiser ownership is validated—including a thrown creative or profile
operation—Apostra first requests paused containment and reads the campaign
back, then requests deletion. It reports rollback only after Snap confirms the
terminal deleted state. If any intervening readback belongs to another
advertiser account, Apostra stops before another provider mutation and
raises an ownership-isolation error. Drift discovered after campaign activation
triggers only a selected-account-scoped paused-containment attempt; if ownership
cannot be recovered, the fatal response says the campaign may still be active
and requires operator review. This also applies when campaign activation returns
an error or its active-state readback fails, because Snap may have applied the
activation. Rejected provider-client promises follow the same cleanup path.
Once another advertiser account is observed, a later provider error cannot
proceed to DELETE until exact selected-account ownership is seen again.
An exact already-deleted selected-account readback
confirms rollback without another DELETE. When Snap cannot confirm deletion, the
response says whether paused containment was confirmed or whether the campaign
may still be active; it never describes an unconfirmed deletion as complete.
Every Snap package or offering must currently resolve to one supported ISO
country; `UK` is normalized to `GB`. Before creating a campaign, Apostra
checks that country against Snap's complete current targeting inventory, cached
for 24 hours. A missing, empty, malformed, or unavailable inventory stops the
request. Five-digit US ZIP targeting is supported. Apostra rejects regions,
device form factors, standard audience include/exclude fields, non-US postal
systems, unknown products, incompatible campaign-objective mixes, and other
targeting it cannot represent exactly before creating the campaign. Product
targeting may use Snap's exact age buckets, one gender, discovered SLC interest
IDs, and discovered Snap audience IDs. Per-individual frequency caps support a
maximum 30-day window on compatible Snap optimization goals. Apostra also
sets Snap's interest and custom-audience expansion controls to disabled so the
provider does not silently broaden those accepted targets.
First-party Customer Lists are isolated to Apostra connection and
selected Snap advertiser that created them. Apostra records a durable
buyer-to-Snap audience identity before writing, validates Snap's exact add and
remove receipts, and confirms deletion only after Snap no longer returns the
segment. Each request carries one identifier type and at most 100,000 hashed
identifiers; an audience reports only counts confirmed by Snap's exact receipts,
including earlier confirmed batches if a later batch fails. The same normalized
identifier cannot be added and removed in one request. Name and description
changes are read back from Snap before
Apostra reports them complete. Hashed identifiers and provider text that
could echo them are excluded from logs together with request paths, bodies, and
provider response data. Member-write errors similarly replace provider and
transport text with fixed host-owned semantics. Polling accepts buyer audience
IDs only and fails as a whole if any requested status cannot be verified.
Post-add/post-remove readback preserves Snap's pending, paused, upload, and
targetability states but does not present Snap's approximate size as an exact
match count. A lost or ambiguous provider response is reported as
reconciling and does not trigger a duplicate create. If a connection has no
durable private identity, audience access fails closed and asks you to reconnect
it. Audience types and tags that Snap cannot preserve are rejected before a
provider request.
Snap recognizes only the documented audience source types. Known sources that
are not first-party Customer Lists remain outside Customer List discovery; an
unknown or malformed source fails the complete read instead of disappearing
silently.
## LinkedIn
Ask the customer to connect the LinkedIn ad account that will run the campaign.
Default grant for activation:
* Ads read access.
* Ads write access.
* Ads reporting access.
* Conversions API access (`rw_conversions`) when the workflow reads conversion
rules or sends server-side events.
Additional access may be needed when creatives or sponsored content require an
organization identity. Apostra does not need Company Page administration unless
the agreed workflow includes organic or organization-social publishing.
Apostra does not need LinkedIn account ownership, billing administration, or
permission to add, edit, or delete users.
### Agency setup
Each client adds the agency's user to their LinkedIn ad account with a role
that can manage campaigns and read reporting (Account Manager, or Campaign
Manager when scoped to campaign work); a Sponsored Content Poster grant on the
client's Page is only needed when ads run under the Page's identity. The
agency connects its own LinkedIn login in Apostra; every client ad account
the login can reach is discovered in the advertiser-mapping area described
above — map each to the right advertiser.
Tracked campaign reads enumerate LinkedIn's complete cursor-paginated
campaign and creative inventory and fail closed if LinkedIn cannot prove a
complete account-scoped snapshot. Campaign inventory is exposed as a read-only
mirror; an account appearing here does not authorize Apostra to mutate its
campaigns.
Measurement is a separate boundary. `sync_event_sources` can discover enabled
`CONVERSIONS_API` rules owned by the selected account, and `log_event` can send
events only to one of those rules. LinkedIn conversion rules are created and
managed in Campaign Manager, not through Apostra. If the connection
predates this capability, reconnect it to grant `rw_conversions` before syncing
event sources; account discovery alone does not authorize event delivery.
## Pinterest
Ask the customer to connect the Pinterest ad account that will run the campaign.
Default grant for activation:
* Ads read access.
* Ads write access.
Additional pin read/write access may be requested when the campaign workflow
uses Pinterest-native creative or pin objects.
Pinterest Conversion Tags are discovered from the selected ad account; an ad
account ID is never substituted for a Tag ID. Apostra shows setup code only when
Pinterest returns the personalized snippet for that Tag. Audience and signal
reads also remain selected-account scoped and fail instead of showing sample
data when Pinterest cannot answer. Server-side event delivery is not currently
available while its signed dedicated-account validation remains outstanding.
Tag discovery returns the complete selected-account inventory or fails; it is
never truncated. A response above 500 Tags or 256 KiB, or a projected result
above 128 KiB, fails before the last proven Tag binding snapshot is replaced.
Apostra does not need account ownership, billing administration, or permission to
add, edit, or delete users.
### Agency setup
The client shares the ad account with the agency's business through Pinterest
business access — adding the agency as a partner by its business ID and
assigning an ad-account role that can manage campaigns and read reporting. The
agency connects its own Pinterest business login in Apostra; shared client
ad accounts are discovered on the **Advertiser mapping** tab — map each to the
right advertiser.
## Reddit
Ask the customer to connect the Reddit Ads account or profile that will run the
campaign.
Default grant for activation:
* Ads read access.
* Ads edit access.
Additional conversion permissions may be requested when the customer enables
conversion tracking or event logging.
Apostra reads the selected account's Reddit Pixel inventory before showing
setup instructions or sending server events. The Pixel ID comes from Reddit;
the ad account ID is not used as a substitute. Server events use Reddit's
Conversions API v3 and are reported as processed only when Reddit confirms the
entire submitted batch. If a Pixel is missing, belongs to another account, or
cannot be resolved to one account, the operation stops before sending events.
Reddit also requires a Pixel on every ad group. Before creating a campaign,
Apostra reads the complete Pixel inventory for the selected ad account. If
there is exactly one authorized Pixel, Apostra uses it. If there are
multiple Pixels, the buyer or agent must select the provider-issued ID with
`context.conversion_pixel_id` and repeat product discovery with that context.
Call `sync_event_sources` without proposed sources to list the authorized Pixel
IDs for the selected account.
Missing, unrecognized, ambiguous, or incomplete Pixel inventory stops the
request before campaign creation. A new campaign's name in Reddit Ads Manager
includes a unique write marker, and Apostra verifies that marker plus the
exact account, paused status, objective, and spend cap before any child or
cleanup mutation. After creating an ad group,
Apostra reads the provider resource again and archives it only after proving
its identity and ownership. An unproven ID is left untouched and alerted while
the known-owned parent campaign is archived. Any cleanup requires exact-identity
terminal readback and reports failure explicitly. After a targeting change, it
accepts the operation only when the same Pixel remains bound. For an older ad
group with no Pixel binding, configure an account-authorized Pixel in Reddit
Ads Manager before changing targeting. That legacy resource violates Reddit's
required ad-group state, so Apostra does not traffic or mutate it. Cleanup
continues if the original request is canceled and is confirmed by reading back
the archived provider state.
Website and app sources keep their native Reddit categories. In-store events
use Reddit's physical-store category; broader offline or CRM events use Reddit's
`OTHER` category because v3 does not accept an `OFFLINE` value.
Customer-list audiences accept SHA-256 email addresses and mobile advertising
IDs. Apostra associates each created list with the connected customer and ad
account, so retries and buyer/provider ID aliases reuse the same Reddit audience
and operation lock. A provider audience created by another connected customer
cannot be changed or deleted through its raw Reddit ID, even when both
connections can access the same ad account. Lists not created by the current
Apostra connection remain visible but read-only. Adds and removes are set operations.
`uploaded_count` reports add rows accepted by Reddit, not matched users;
audience size and match readiness come from later provider status. A failed
delete or remove is returned as failed rather than successful.
Reddit does not return partner ad accounts when an integration enumerates a
Business. Account-selected Pixel discovery still works for those accounts, but
server-event logging without an account reference fails closed until the Pixel
can be connected to one accessible account.
Apostra does not need Reddit account ownership, profile administration beyond
the advertising workflow, or permission to add, edit, or delete users.
### Agency setup
Each client invites the agency's user into their Reddit Ads Business as a
member with access to the relevant ad account and a role that can manage ads
and read reporting. The agency connects its own Reddit login in Apostra;
the client ad accounts the login can reach are discovered grouped under their
Business on the **Advertiser mapping** tab — map each to the right advertiser.
## Spotify
Ask the customer to authorize Spotify with a user who can reach the Business
and advertiser ad account that will run the campaign.
Default grant for activation:
* Campaign and ad-set management access.
* Reporting access for the selected advertiser account.
The Spotify adapter can pause or resume a campaign and update package budgets,
bids, end dates, and delivery state through the canonical buyer media-buy
lifecycle. Its protected canary queries products, creates a future-dated buy,
pauses it immediately, verifies readback, performs a bounded package-budget
decrease, and confirms the result through the same buyer APIs used by regular
traffic.
Spotify does not expose an irreversible campaign
cancellation operation: use pause to stop delivery, and expect a cancellation
request to return unsupported rather than permanently deleting the campaign.
Configure Spotify Pixel and Conversions API sources in Spotify Ads Manager.
Apostra discovers only sources delegated to the selected advertiser account
and does not create a new source against an unverified dataset. Spotify Pixel is
a browser installation managed in Spotify Ads Manager; Conversions API is the
server-side source. Customer-list discovery and reconciled deletion are available.
Customer-list upload and replacement are currently unavailable because Spotify's
published Ads API does not provide the CSV and retry-recovery contract needed to
make those writes safe. Conversion-event ingestion is not currently advertised:
the shared request cannot select an advertiser account for Spotify's multi-account
OAuth connection, and Spotify does not document authoritative success semantics
that make retries safe. Calls fail before provider I/O.
Apostra does not need Spotify Business ownership, billing administration, or
permission to add, edit, or delete users.
## Amazon Ads
Ask the customer to connect the Amazon Ads account that will run the campaign.
Default grant for activation:
* Advertising campaign-management access.
* Reporting access.
Amazon retail, seller, or vendor account administration is separate from
advertising campaign management. Apostra does not need retail account ownership,
billing administration, or permission to add, edit, or delete users.
### Agency setup
Each client invites the agency's user to their Amazon advertising account from
the Amazon Ads console's user management, with a role that can manage
campaigns and read reporting (DSP entities manage their users separately).
Amazon access follows the Login with Amazon user, so the agency connects its
own Amazon login in Apostra; every Sponsored Ads profile and DSP
advertiser the login can reach is discovered on the **Advertiser mapping**
tab. Amazon has no manager container — accounts arrive as a flat list, typed
`sponsored_ads` or `dsp`.
## Revoking Access
Customers can revoke Apostra's access from the platform's business, partner,
manager-account, or OAuth settings. Revoking access stops future discovery,
campaign changes, creative sync, audience sync, catalog sync, reporting reads,
and measurement writes for that connected platform account.
After revocation, refresh the connection in Apostra before running new
discovery or media-buy workflows so the account status reflects the provider's
current authorization state.
# Data Delivery
Source: https://docs.apostra.com/v2/guides/data-delivery
Ship log-level delivery data (impressions, clicks, conversions, MMP postbacks, VAST events, MB delivery) from Apostra to a buyer-owned GCS bucket, S3 bucket, or Azure Blob container on a recurring cadence.
## Overview
Data Delivery is a standing subscription that pushes log-level data (LLD) from Apostra into a destination you own — your cloud storage, your IAM, your data pipeline. Unlike `/api/v2/buyer/reporting/metrics` (which returns aggregates synchronously), Data Delivery runs on a schedule, writes objects, and never makes you pull.
Two objects make it work, both scoped to an advertiser:
| Object | What it is |
| - | - |
| **Data Delivery Credential** | A named handle that tells Apostra *where* to write and how to authenticate. Probed asynchronously; carries a `status` of `PENDING`, `VALIDATED`, or `FAILED`. |
| **Data Delivery Output** | A standing subscription that says "ship `` to `` every `` as `` under ``". Multiple Outputs can share a credential. Campaign-scoped Outputs override the advertiser default for the same data type. |
Available data types: `MB_DELIVERY`, `IMPRESSIONS`, `CLICKS`, `VAST_EVENTS`, `CAPI_ATTRIBUTION`, `MMP_POSTBACKS`. Cadences: `HOURLY` (minute 0), `DAILY` (00:00 UTC), `WEEKLY` (00:00 UTC on `syncWeeklyDay`). Formats: `JSONL`, `PARQUET`, `CSV`.
Limits: up to 20 credentials and 20 Outputs per advertiser. One Output per `(dataDeliveryType, credentialName)` pair per scope — to fan a data type out to two destinations, list one Output per destination.
```
BASE = https://api.apostra.com/api/v2/buyer
AUTH = Authorization: Bearer scope3_
```
## End-to-end shape
The high-level flow is the same regardless of destination:
Add a bucket/container that Apostra can write into and grant Apostra principal the required permissions. Details per destination below.
`PUT /advertisers/:advertiserId` with a `dataDelivery.credentials[]` entry. The credential lands as `status: PENDING`.
Apostra asynchronously writes a sentinel object under `_scope3-probe/` and deletes it. The credential flips to `VALIDATED` on success or `FAILED` (with `statusError`) on failure.
`PUT /advertisers/:advertiserId` with `dataDelivery.outputs[]` referencing the credential by `credentialName`. Optionally override per campaign via `PUT /campaigns/:campaignId` `dataDelivery.outputs[]`.
Each Output fires on its schedule and lands objects under `//...`. Open the bucket/container to confirm.
`dataDelivery.credentials` and `dataDelivery.outputs` are **full-replace arrays**. The request body is the new desired state — anything not listed is archived (credentials) or cleared (Outputs). Read the current state with `GET /advertisers/:advertiserId` before writing. A credential cannot be archived while any live Output (advertiser- or campaign-scoped) still references it — remove or repoint the Output first.
## Destination setup
### 1. Grant access on your bucket
Apostra authenticates to GCS with a single service account. Grant it `roles/storage.objectCreator` on the bucket you want data shipped into. No keys or secrets are exchanged.
**Apostra service account:**
```
report-delivery@swift-catfish-337215.iam.gserviceaccount.com
```
Grant via `gcloud`:
```bash theme={null}
gcloud storage buckets add-iam-policy-binding gs://my-apostra-data \
--member="serviceAccount:report-delivery@swift-catfish-337215.iam.gserviceaccount.com" \
--role="roles/storage.objectCreator"
```
### 2. Register the credential
```bash theme={null}
curl -X PUT "$BASE/advertisers/12345" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"dataDelivery": {
"credentials": [
{
"name": "primary-gcs",
"config": {
"type": "GCS",
"bucket": "my-apostra-data"
}
}
]
}
}'
```
`name` must be unique among live credentials on this advertiser. Outputs will reference it by this name.
### 1. Grant access via bucket policy
Apostra authenticates to S3 with a shared IAM user. Add a statement to your bucket policy granting that principal `s3:PutObject` and `s3:DeleteObject` on objects under the path prefix you'll use. No per-buyer access keys are needed.
**Apostra IAM principal:**
```
arn:aws:iam::948454267882:user/scope3-data-sync-service
```
Example bucket policy statement (replace `my-apostra-data`):
```json theme={null}
{
"Sid": "AllowApostraDataDelivery",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::948454267882:user/scope3-data-sync-service"
},
"Action": [
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-apostra-data/*"
}
```
`s3:DeleteObject` is recommended so the Probe can clean up its `_scope3-probe/` sentinel objects; with `s3:PutObject` only, sentinels accumulate but the credential still validates.
### 2. Register the credential
```bash theme={null}
curl -X PUT "$BASE/advertisers/12345" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"dataDelivery": {
"credentials": [
{
"name": "primary-s3",
"config": {
"type": "S3",
"bucket": "my-apostra-data",
"region": "us-east-1"
}
}
]
}
}'
```
`region` accepts any commercial AWS region (e.g., `us-east-1`, `eu-west-1`). GovCloud and ISO partitions are not currently supported — contact Apostra if you need them.
### 1. Mint a SAS for the container
Apostra authenticates to Azure with a **Shared Access Signature (SAS)**. Either flavor works — pick the one your security tooling produces.
In both flavors, the required permissions are **Create + Write + Delete** (`sp` includes `c`, `w`, `d`). The Probe and delivery worker perform `PUT Blob`, block uploads, and `DELETE Blob` — they do not use Append or List.
#### Option A — Service SAS scoped to the container (recommended)
Narrower grant; the SAS is scoped to one container, with no implicit account-level access.
```bash theme={null}
az storage container generate-sas \
--account-name datareports \
--name apostra-data \
--permissions cwd \
--expiry 2027-01-01T00:00:00Z \
--https-only \
--output tsv
```
The resulting SAS has `sr=c&sp=cwd` — `sr=c` scopes it to the container and `sp=cwd` authorizes blob operations inside it. **No `srt` field.**
A **blob-scoped Service SAS** (`sr=b`) does **not** work. The Probe writes to a randomized `_scope3-probe/.txt` path; a blob-scoped SAS is limited to one fixed blob name and the URLs will not match. Always scope Service SAS to the container (`sr=c`).
#### Option B — Account SAS
If your org policy requires Account SAS instead, the `srt` field **must include `o` (Object)** — the Probe and the delivery worker perform object-level operations (`PUT Blob`, `DELETE Blob`). `srt=co` is the common shape; `srt=o` alone also works.
```bash theme={null}
az storage account generate-sas \
--account-name datareports \
--services b \
--resource-types co \
--permissions cwd \
--expiry 2027-01-01T00:00:00Z \
--https-only \
--output tsv
```
The resulting SAS has `ss=b&srt=co&sp=cwd`. `srt=s` (Service only) or `srt=c` (Container only) **will not work** — Container-only authorizes operations *on* the container resource, not on the blobs inside it.
The SAS token is a secret. Submit only over HTTPS and never paste it into shared docs or tickets. Apostra stores it encrypted, redacts it from all `GET` responses, and surfaces only the parsed `expiresAt` back.
### 2. Register the credential
```bash theme={null}
curl -X PUT "$BASE/advertisers/12345" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"dataDelivery": {
"credentials": [
{
"name": "primary-azure",
"config": {
"type": "AZURE_BLOB",
"storageAccountName": "datareports",
"containerName": "apostra-data",
"auth": {
"mode": "SAS_TOKEN",
"sasToken": "sv=2022-11-02&se=2027-01-01T00%3A00%3A00Z&sr=c&sp=cwd&sig=..."
}
}
}
]
}
}'
```
Leading `?` on the SAS is tolerated and stripped. The `se=` claim is parsed and surfaced as `credential.expiresAt` — see [Rotation](#rotation) below.
## Probe lifecycle
When a credential is created or its config changes, Apostra kicks off an async **Probe** workflow keyed on the credential. The Probe writes a sentinel object to the destination, deletes it, and updates the credential row:
| `status` | Meaning |
| - | - |
| `PENDING` | Just created or rotated; Probe hasn't completed yet. Typically clears within seconds. |
| `VALIDATED` | Probe wrote and deleted a sentinel object. `validatedAt` reflects the run. |
| `FAILED` | Probe could not write. `statusError` carries a human-readable reason. |
Check status with:
```bash theme={null}
curl "$BASE/advertisers/12345" \
-H "Authorization: Bearer scope3_"
# inspect dataDelivery.credentials[].status / statusError
```
Re-run the Probe without re-submitting the credential:
```bash theme={null}
curl -X POST "$BASE/advertisers/12345/data-delivery-credentials/primary-gcs/validate" \
-H "Authorization: Bearer scope3_"
```
Outputs can reference a credential in any status. The shipping workflow re-checks at run time, so a credential that flips to `FAILED` will block its Outputs until you fix it; one that flips back to `VALIDATED` resumes automatically.
## Attaching Outputs
Outputs live alongside credentials in the same `dataDelivery` block. The example below ships hourly impressions and daily clicks to the same GCS credential, with each data type writing under a distinct path prefix.
```bash theme={null}
curl -X PUT "$BASE/advertisers/12345" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"dataDelivery": {
"outputs": [
{
"dataDeliveryType": "IMPRESSIONS",
"cadence": "HOURLY",
"credentialName": "primary-gcs",
"deliveryConfig": {
"type": "GCS",
"pathPrefix": "apostra/impressions/",
"format": "PARQUET"
}
},
{
"dataDeliveryType": "CLICKS",
"cadence": "DAILY",
"credentialName": "primary-gcs",
"deliveryConfig": {
"type": "GCS",
"pathPrefix": "apostra/clicks/",
"format": "JSONL"
}
}
]
}
}'
```
### Rules to know
* **`credentialName` must point at a live credential on the same advertiser.** The `deliveryConfig.type` must match the credential's `destinationType` (you can't ship to S3 through a GCS credential).
* **One Output per `(dataDeliveryType, credentialName)`.** To send the same data type to two destinations, list two Outputs with different credentials.
* **`syncWeeklyDay` is required when `cadence: "WEEKLY"`** (0 = Sunday, 6 = Saturday). Ignored for HOURLY/DAILY.
* **`pathPrefix` is used verbatim** — leading slashes are not stripped and the prefix is not templated. End it with `/` if you want a directory-like layout.
* **`enabled: false`** pauses the schedule without removing the Output. In-flight runs complete; no new runs fire until you flip it back.
### Campaign-scoped overrides
For a single campaign that needs a different cadence, format, or destination than its advertiser default, set `dataDelivery.outputs` on the campaign:
```bash theme={null}
curl -X PUT "$BASE/campaigns/99999" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"dataDelivery": {
"outputs": [
{
"dataDeliveryType": "IMPRESSIONS",
"cadence": "HOURLY",
"credentialName": "primary-gcs",
"deliveryConfig": {
"type": "GCS",
"pathPrefix": "apostra/campaign-99999/impressions/",
"format": "PARQUET"
}
}
]
}
}'
```
The override replaces the advertiser-scoped Output for the matching `dataDeliveryType` on this campaign only. Resolved Outputs in `GET` responses are tagged with `source: "advertiser"` or `source: "campaign"` so it's clear which level produced each entry.
## Rotation
### GCS and S3
No rotating secret to manage — Apostra's identity is fixed, so as long as the IAM grant stays in place the credential keeps working. Rotating means changing the bucket: submit a new credential with the new `bucket` and the existing credentials inline-array will update in place.
### Azure Blob
SAS tokens carry an expiry. The expiry from the `se=` claim is parsed and exposed on the credential response as `expiresAt` — watch it and rotate ahead of time.
To rotate, submit a fresh credential **with the same `name`**:
```bash theme={null}
curl -X PUT "$BASE/advertisers/12345" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"dataDelivery": {
"credentials": [
{
"name": "primary-azure",
"config": {
"type": "AZURE_BLOB",
"storageAccountName": "datareports",
"containerName": "apostra-data",
"auth": { "mode": "SAS_TOKEN", "sasToken": "" }
}
}
]
}
}'
```
Updating in place preserves the `credentialId`, so any Output that references this credential by name keeps shipping without interruption. A fresh Probe runs against the new SAS; an in-flight Probe for the old SAS is cancelled.
## Troubleshooting
### Credential stuck on `PENDING`
The Probe workflow typically completes within seconds. If it's been more than a minute:
1. Re-fetch the advertiser — the row may have already flipped.
2. Call the revalidate endpoint to force a fresh Probe.
3. If it stays `PENDING`, contact Apostra support — the Probe worker may be backed up.
### `status: FAILED` — common `statusError` patterns
| Destination | `statusError` includes... | Fix |
| - | - | - |
| GCS | `permission`, `403`, `does not have storage.objects.create` | Grant `roles/storage.objectCreator` to `report-delivery@swift-catfish-337215.iam.gserviceaccount.com` on the bucket. |
| GCS | `bucket ... does not exist` | Check the bucket name in `config.bucket` matches an existing bucket in the project that hosts the SA. |
| S3 | `AccessDenied`, `403` | Confirm the bucket policy grants `s3:PutObject` to `arn:aws:iam::948454267882:user/scope3-data-sync-service` on `Resource: arn:aws:s3:::/*`. |
| S3 | `PermanentRedirect`, `region` mismatch | `config.region` must match the bucket's actual region. |
| S3 | `NoSuchBucket` | Bucket name typo, or the bucket isn't in a commercial AWS partition. |
| Azure | `Azure SAS token expired at ...` | The `se=` claim is in the past. Mint a new SAS and update the credential. |
| Azure | `AuthenticationFailed` | SAS signature is wrong (truncated paste, wrong `sv=` version, signed against a different key). Regenerate. |
| Azure | `AuthorizationPermissionMismatch` | SAS is valid but missing required permissions. Confirm `sp` includes `c`, `w`, `d` (`sp=cwd`). For Account SAS, also confirm `ss=b`. |
| Azure | `AuthorizationResourceTypeMismatch` | **Account SAS only** — `srt` is missing `o` (Object). The Probe and delivery worker perform blob-level operations, so `srt` must include `o` (e.g., `srt=co`). `srt=s` or `srt=c` alone will not work. Regenerate. |
| Azure | `AuthorizationFailure` on `PUT //_scope3-probe/.txt` | Service SAS was scoped to a single blob (`sr=b`) instead of the container (`sr=c`). Regenerate with `az storage container generate-sas` so the SAS covers any blob name under the container. |
| Azure | `ContainerNotFound` | `containerName` doesn't exist on the storage account, or the SAS was issued at a different scope. |
### Output created but no objects landing
1. Verify the referenced credential is `VALIDATED`.
2. Check `enabled` on the Output — if `false`, the schedule is paused.
3. For HOURLY cadence, allow a full hour boundary to pass before the first run. DAILY fires at 00:00 UTC, WEEKLY at 00:00 UTC on `syncWeeklyDay`.
4. Confirm there is delivery data for the period — a campaign with zero impressions on a given day produces no objects, not an empty file.
### Conflicting Outputs
If `PUT` returns a validation error about duplicate `(dataDeliveryType, credentialName)` pairs, it's because the inline array contains two Outputs with the same data type pointing at the same credential. Use distinct credentials, or merge into a single entry.
## Related
* [Reporting Overview](/v2/guides/reporting-overview) — pull-style aggregate metrics (the synchronous counterpart to Data Delivery).
* [Log Events](/v2/guides/log-events) — push events *into* Apostra (the inbound counterpart to outbound Data Delivery).
# Product Discovery
Source: https://docs.apostra.com/v2/guides/discovery
Discover available media products from agents, evaluate proposals, and apply them to your campaigns
## Overview
Building a buyer agent or testing AdCP behavior? Start with [Get products
across storefronts](/v2/buyer/discovery/multi-storefront-get-products). It
returns a bounded canonical product/proposal page, can query named or all
connected storefronts, and optionally screens and refines proposals from
buyer instructions. Use the Discover Products compatibility flow below when
you need grouped pagination, summary/budget context, browse/SSE, or an
existing `discoveryId` workflow. It projects the same raw fan-out and optional
screen; it does not invoke a separate ranking brain.
Product Discovery is the buyer-side loop for finding inventory across one or more
sales agents (publishers, exchanges, content owners). You provide a brief — what
you're trying to accomplish, who you're targeting, when it runs, and how much
you can spend — and Apostra fans the request out to every agent your advertiser has
access to. The agents return products and (when supported) recommended
**proposals** that allocate budget across those products.
The flow:
For storefronts that gate inventory, register per-source credentials first — see the [Storefront object guide](/v2/object-guides/storefront#registering-credentials-per-source).
`POST /api/v2/buyer/discovery/discover-products` with a brief. Returns a
`discoveryId` plus product groups, agent proposals, and budget context.
Page through results with `GET /api/v2/buyer/discovery/{id}/discover-products`
or iterate by sending `refine` instructions back to the same `discoveryId`.
Add specific products with `POST /api/v2/buyer/discovery/{id}/products` or
apply a full proposal with `POST /api/v2/buyer/discovery/{id}/apply-proposal`.
Call `create_media_buys` to stage or execute several qualified selections
at once. Supply an existing campaign or let the operation create the DRAFT
campaign cart. `auto-select-products` remains available for performance
campaigns.
A discovery execution is the long-lived search and refinement record identified
by `discoveryId` or `get_products.ext.interchange.execution_id`. You can re-open
it and refine results without losing context. The DRAFT campaign is the durable
shopping cart: it owns selected proposals/products and every media buy. Legacy
session-selection endpoints project selections through the same reconciliation
path for compatibility.
## Progressive delivery
Discovery fans out to many sellers, and they answer at very different speeds —
warmed catalogs in milliseconds, live agents in tens of seconds. By default the
response waits for everyone (bounded by `waitMode`/`waitSeconds`). Pass
`progressive: true` and sellers' answers arrive as they land instead: the first
response returns within a couple of seconds with whoever has answered,
`resultsComplete: false`, and a `pendingAgents` list; you then poll
[Browse products](/v2/buyer/discovery/tasks/browse-products) with
`sinceRevision` to receive only newly landed seller groups until
`resultsComplete` is true. Every progressive response includes a `guidance`
string spelling out the exact next call, so agent clients need no special
handling. The complete worked sequence is in
[Discover products → Progressive delivery](/v2/buyer/discovery/tasks/discover-products#progressive-delivery).
In Apostra chat, Murph prefers `get_products` for new canonical
product/proposal exploration and uses quick replacement snapshots while sellers
answer. Murph uses `discover_products` when the buyer explicitly asks for the
legacy grouped/session presentation or continues an existing `discoveryId`
workflow. It does not call both surfaces by default.
## Storefront agents in discovery
Discovery fans out to both third-party sales agents and Apostra storefront agents. A storefront agent has `salesAgentId: "storefront-{platform_id}"`, where `platform_id` is the storefront's public `platformId` slug, and represents the storefront as a buyer-facing route for product discovery and media-buy execution. That ID identifies the storefront surface; it is not a source ID or underlying source agent ID.
Buyers call the same discovery endpoint for all agents. They do not need to distinguish between a storefront on the Storefront-built path, which composes products from raw inventory, and a storefront on the Agent-supplied path, which passes requests through to an upstream inventory source. Composition storefronts return products assembled from active inventory sources and active operating instructions. Storefronts on the Agent-supplied path proxy `get_products` to an active source and apply storefront metadata and buyer-instruction overlays before results are returned. Buyer instructions are resolved from operator domain, brand domain, and optional country. Use `storefrontIds` or `storefrontNames` when you want to restrict discovery to specific storefronts.
If a seller's catalog and connection updates arrive close together, Apostra
keeps the newest usable result so available inventory does not briefly
disappear. An older update finishing late cannot replace newer evidence. This
does not override storefront visibility, source credentials, request
eligibility, or a seller's current unhealthy state.
Public buyer discovery only includes Storefronts that have passed marketplace
review and are listed. A Storefront can be live for known transactions while it
is still pending review, but it will not appear in public discovery results or
buyer marketplace browsing until an admin lists it.
### Murph account analysis for adapter storefronts
Operators can ask Murph to run `analyze_account` for adapter-routed
storefronts with delegated provider credentials. The tool analyzes a connected
advertising account through the storefront adapter and can render an
interactive account-analysis viewer in compatible MCP clients.
Inputs:
| Field | Type | Notes |
| - | - | - |
| `provider` | string | Optional for self-service. Required when a super admin needs to disambiguate a client storefront. Supported values are the providers that can hold delegated credentials: `amazon`, `google`, `linkedin`, `meta`, `pinterest`, `reddit`, `snap`, `spotify`, and `tiktok`. |
| `targetCustomerId` | integer | Super-admin-only account ID whose adapter storefront should be analyzed. |
| `storefrontPlatformId` | string | Super-admin-only public storefront platform ID to analyze instead of `targetCustomerId`. |
| `accountId` | string | Optional upstream provider account ID to request from the connected delegated credential. |
The response includes an `analysis` object returned by the adapter, plus a
`recommendations[]` array for the operator. Adapter implementations may include
findings, metrics, account health signals, and other provider-specific details
inside `analysis`.
`analyze_account` is read-only but requires the storefront to be adapter-routed
and connected with delegated provider auth. Without a stored delegated
credential, Murph returns an auth-required response that includes the provider
OAuth setup path. Cross-account targeting is restricted to SuperAdmins; normal
storefront operators can analyze only their own storefront.
## Public vs private storefronts
Storefronts fall into two visibility tiers:
| Tier | Who can discover | How to access |
| - | - | - |
| **Public** | Any authenticated buyer | Available by default after marketplace review |
| **Private / gated** | Buyers with credentials registered for the source | Register credentials per inventory source (API key, OAuth, or JWT) |
Public discovery is also gated by marketplace review: pending-review and hidden
Storefronts are omitted from public buyer discovery even if they are otherwise
live. Private storefronts gate inventory behind per-source authentication.
Apostra speaks to all sources - public and private - over the **Ad Context
Protocol (ADCP)**; the difference is just whether the source requires the buyer
to present credentials before products are returned. The buyer never needs an
AAO key directly: AAO registry and compliance signals are handled server-side by
Apostra.
## Step 1: Connect private storefronts (optional)
If you only need public inventory, skip ahead to [Step 2](#step-2-run-discovery).
To unlock private storefronts, register credentials for the relevant inventory
source. Three auth types are supported (API key, OAuth, JWT) — the source
declares which it requires. Full walkthrough lives in the [Storefront object
guide](/v2/object-guides/storefront#registering-credentials-per-source); the
short version:
```bash theme={null}
curl -X POST \
'https://api.apostra.com/api/v2/buyer/storefronts/42/sources/agt_premium_ctv/credentials' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{ "auth": { "type": "api_key", "token": "" } }'
```
For OAuth-secured sources, request an authorize URL and complete the buyer
consent flow — Apostra hosts the callback at `/api/v2/storefront/oauth/callback`
so AI agents and back-end clients don't need to handle redirects themselves.
Once credentials are registered for a source, every discovery call from your
account automatically queries it — no per-call configuration needed.
## Step 2: Run discovery
`POST /api/v2/buyer/discovery/discover-products` is the main entry point. You
can pass a brief inline or seed the request from an existing campaign with
`campaignId`.
### Request
```bash theme={null}
curl -X POST 'https://api.apostra.com/api/v2/buyer/discovery/discover-products' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"advertiserId": 12345,
"brief": "Premium CTV inventory for Q3 sports launch — adults 25-54, US only, video creative",
"budget": 250000,
"channels": ["ctv", "olv"],
"countries": ["US"],
"flightDates": {
"startDate": "2026-07-01T00:00:00Z",
"endDate": "2026-09-30T23:59:59Z"
},
"groupLimit": 10,
"productsPerGroup": 10
}'
```
| Field | Type | Description |
| - | - | - |
| `advertiserId` | integer | **Required.** Resolves the brand reference used to score agent results. |
| `discoveryId` | string | Reuse an existing session. Required when sending `refine`. |
| `campaignId` | string | Seed brief, flight dates, and budget from a campaign. Inline values override. |
| `brief` | string (≤5000 chars) | Natural-language search context. |
| `budget` | number | Total budget in account currency. Used for budget context + proposals. |
| `channels` | string\[] | `display`, `olv`, `ctv`, `social`, or `video` (alias for `olv`). Defaults to `["display","olv","ctv","social"]`. |
| `countries` | string\[] | ISO 3166-1 alpha-2 country codes. Defaults to brand agent countries. |
| `flightDates` | `{ startDate, endDate }` | ISO 8601 datetimes (e.g. `2026-07-01T00:00:00Z`). Filters by agent availability. |
| `publisherDomain` | string | Filter to a single publisher domain. |
| `pricingModel` | string | Filter by AdCP pricing model (`cpm`, `cpcv`, etc.). |
| `storefrontIds` | integer\[] | Restrict to specific storefronts (IDs from `list_storefronts`). Empty array = same as omitted (no request-level filter); falls back to the campaign-level pin if a `campaignId` is also provided. |
| `storefrontNames` | string\[] | Restrict to storefronts whose name matches (case-insensitive substring). Empty array = same as omitted; falls back to the campaign-level pin if available. |
| `groupLimit` | integer (≤10) | Max product groups per page. Default 10. |
| `productsPerGroup` | integer (≤15) | Max products inside each group. Default 10. |
| `groupOffset` / `productOffset` | integer | Pagination cursors. |
| `debug` | boolean | Include per-agent ADCP request/response logs in the response. |
| `screening` | object | Optional plain-language screening instructions plus refinement/pass limits. Uses the same managed accept, reject, and seller-refinement engine as buyer `get_products`. |
| `refine` | array | Refinement directives (see [Refining](#refining-results)). Requires `discoveryId`. |
### Response
```json theme={null}
{
"discoveryId": "disc_01HZX3YQ7K9R6V3M2P1E0F8B2T",
"productGroups": [
{
"groupId": "ctx_disc_01HZX3-group-0",
"groupName": "Acme Media Connected TV",
"description": "Premium CTV across Acme Media's owned-and-operated apps",
"productCount": 4,
"totalProducts": 12,
"hasMoreProducts": true,
"products": [
{
"productId": "prod_acme_ctv_sports",
"name": "Acme Sports CTV — 30s",
"channel": "ctv",
"formatTypes": ["video", "30s"],
"cpm": 32.5,
"salesAgentId": "agent_acme_media",
"salesAgentName": "Acme Media Ad Sales",
"deliveryType": "guaranteed",
"briefRelevance": "Lives squarely on Acme Media's sports verticals — adults 25-54 over-index here",
"pricingOptions": [
{ "pricingOptionId": "po_fixed_30", "pricingModel": "cpm", "isFixed": true, "fixedPrice": 32.5, "currency": "USD" }
],
"estimatedExposures": 7700000
}
]
}
],
"totalGroups": 18,
"hasMoreGroups": true,
"summary": {
"totalProducts": 142,
"publishersCount": 18,
"priceRange": { "min": 4.5, "max": 78.0, "avg": 21.4 }
},
"budgetContext": {
"sessionBudget": 250000,
"allocatedBudget": 0,
"remainingBudget": 250000
},
"proposals": [
{
"proposalId": "proposal_acme_q3_sports",
"name": "Q3 Sports — premium CTV mix",
"salesAgentName": "Acme Media Ad Sales",
"briefAlignment": "Combines guaranteed sports with non-guaranteed sports-adjacent OLV to extend reach",
"allocations": [
{ "productId": "prod_acme_ctv_sports", "allocationPercentage": 60, "rationale": "Anchors the plan with guaranteed delivery" },
{ "productId": "prod_acme_olv_sports", "allocationPercentage": 40 }
],
"totalBudgetGuidance": { "min": 150000, "recommended": 250000, "max": 400000, "currency": "USD" }
}
]
}
```
The first call always creates a new session unless you pass `discoveryId`.
Persist the returned `discoveryId` — every subsequent call references it.
Discovery fans out across all reachable agents in parallel. Slow agents do
not block fast ones; agents that fail or are skipped are surfaced under
`agentResults` only when you pass `debug: true`.
Agents whose advertised channel coverage does not overlap with the requested
channels are skipped before fanout (no round-trip), and appear in
`agentResults` with a `skipReason` like
`Agent does not sell requested channels (supports: display, ctv; requested: social)`.
For agents that respond with an error, `skipReason` carries the
human-readable rejection text from the agent (e.g. `"We do not support the
list of channels you specified"`); prefer it over `error` when surfacing the
reason in a UI. `skipReason` is agent-controlled content, sanitize before
rendering as HTML.
Sellers operating third-party sales-agent inventory sources can inspect the
source-side view in [Diagnose third-party sales agents](/v2/storefront/inventory-sources/diagnostics).
## Step 3: Page through results
Use the GET endpoint to browse the same session without spending another
LLM-enriched discovery call. Filters narrow the cached result set in place.
```bash theme={null}
curl -X GET 'https://api.apostra.com/api/v2/buyer/discovery/disc_01HZX.../discover-products?groupOffset=10&publisherDomain=acmemedia&pricingModel=cpm' \
-H 'Authorization: Bearer scope3_'
```
Query parameters mirror the discover request: `groupLimit`, `groupOffset`,
`productsPerGroup`, `productOffset`, `publisherDomain`, `pricingModel`,
`storefrontIds`, `storefrontNames`, `debug`.
### Refining results
Iterate on a previous response by sending `refine` instructions back to the
same `discoveryId`. Refinements come in three scopes:
```json Request-scoped theme={null}
{
"advertiserId": 12345,
"discoveryId": "disc_01HZX...",
"refine": [
{ "scope": "request", "ask": "Less display, more video. Drop anything below $10 CPM." }
]
}
```
```json Product-scoped theme={null}
{
"advertiserId": 12345,
"discoveryId": "disc_01HZX...",
"refine": [
{ "scope": "product", "id": "prod_acme_ctv_sports", "action": "more_like_this", "ask": "Same audience but with 16:9 social format" },
{ "scope": "product", "id": "prod_random_display", "action": "omit" }
]
}
```
```json Proposal-scoped theme={null}
{
"advertiserId": 12345,
"discoveryId": "disc_01HZX...",
"refine": [
{ "scope": "proposal", "id": "proposal_acme_q3_sports", "action": "finalize" }
]
}
```
Treat every returned product or proposal `id` as opaque and send that exact
value back when refining. Apostra returns every Storefront-sourced product
with an `sf2`-qualified product ID that identifies the exact Storefront and
source. When multiple inventory sources expose the same raw product ID, a bare
product ID is rejected as ambiguous; reuse the returned `sf2` value instead.
Proposal IDs use `sfp1` and identify a Storefront, not an inventory source. If
two sources in that Storefront return the same local proposal ID, Apostra
cannot return an unambiguous proposal ID and withholds both proposals until the
seller returns distinct IDs.
The agent's reply to each instruction comes back under `refinementApplied`
(matched by position) with `status: "applied" | "partial" | "unable"` and an
optional explanation.
You can include `screening` on the same request. Apostra first applies the
explicit seller refinement, then runs the refined proposals through the same
managed screening and bounded autonomous-refinement loop used by multi-storefront
`get_products`. Discover returns the accepted snapshot in its existing grouped
response shape. Screened Discover calls are terminal; use buyer `get_products`
when you need progressive screened replacement pages while sellers are still
responding.
A proposal-scoped `finalize` asks the seller to commit the draft plan: the
seller re-validates that every product in the proposal is still available at
current pricing, then answers `status: "applied"` with the commitment window
(the committed proposal's `expires_at`). A committed proposal is a price and
composition commitment — not an inventory hold — and can be executed directly
by passing its `proposal_id` and a `total_budget` to `create_media_buy`
before it expires. Each committed proposal executes at most once; a proposal
whose products have changed since the draft answers `status: "unable"` with
the reason, and you can re-run discovery for a fresh plan.
## Step 4: View specific products
Add products you want to evaluate to the session. Each selection records the
`productId`, the `salesAgentId` it came from, and the group it was discovered
in. Optionally pin a budget allocation, pricing option, or bid.
```bash theme={null}
curl -X POST 'https://api.apostra.com/api/v2/buyer/discovery/disc_01HZX.../products' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"products": [
{
"productId": "prod_acme_ctv_sports",
"salesAgentId": "agent_acme_media",
"groupId": "ctx_disc_01HZX3-group-0",
"groupName": "Acme Media Connected TV",
"pricingOptionId": "po_fixed_30",
"budget": 150000
}
]
}'
```
For non-fixed pricing, include `bidPrice` (read it from the product's
`pricingOptions[].rate` or `floorPrice` in the discovery response).
List the current session selection at any time:
```bash theme={null}
curl -X GET 'https://api.apostra.com/api/v2/buyer/discovery/disc_01HZX.../products' \
-H 'Authorization: Bearer scope3_'
```
Pull a single product's full detail (including extended specs from the sales
agent) with the per-product detail endpoint:
```bash theme={null}
curl -X GET 'https://api.apostra.com/api/v2/buyer/discovery/disc_01HZX.../products/prod_acme_ctv_sports/details?salesAgentId=agent_acme_media' \
-H 'Authorization: Bearer scope3_'
```
Remove products you no longer want:
```bash theme={null}
curl -X DELETE 'https://api.apostra.com/api/v2/buyer/discovery/disc_01HZX.../products' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{ "productIds": ["prod_random_display"] }'
```
## Step 5: Apply a proposal
When an agent returns a proposal, you can accept its full allocation in one
call instead of adding products one at a time.
```bash theme={null}
curl -X POST 'https://api.apostra.com/api/v2/buyer/discovery/disc_01HZX.../apply-proposal' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"proposalId": "proposal_acme_q3_sports",
"totalBudget": 250000,
"replace": true
}'
```
| Field | Description |
| - | - |
| `proposalId` | Required. ID from the discover-products response. |
| `totalBudget` | Optional. Defaults to `proposal.totalBudgetGuidance.recommended`. |
| `replace` | When `true`, clears existing selected products before applying. |
The response echoes the applied proposal, the budget actually distributed, the
products that were added, and any products from the proposal that could not be
matched back to the discovery results (`productsSkipped`).
Proposal IDs are unique only within a Storefront. If two inventory sources in
the same Storefront return the same local proposal ID, Apostra withholds
both proposals because the public proposal ID cannot identify one source
unambiguously. The seller must return distinct proposal IDs before either plan
can be applied.
While an inventory source in a Storefront is still responding, proposals from
that Storefront may be absent from an in-progress response. They appear after
Apostra can verify which source returned each proposal.
`productsSkipped` is non-empty when an agent's proposal references a product
that has aged out of the cached discovery results. Re-run discover to refresh
the session, then re-apply.
## Auto-select on a campaign
For agentic / hands-off flows, attach an existing campaign and let Apostra pick
products for you. This wraps discovery + selection in a single call against the
campaign's existing brief, flight dates, and budget.
```bash theme={null}
curl -X POST 'https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/auto-select-products' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"maxProducts": 5,
"minBudgetPerProduct": 5000
}'
```
The body is optional — omit it for a fully automated first pass. To iterate,
send back the same `refine` directives accepted by `discover-products`:
```json theme={null}
{
"refine": [
{ "scope": "request", "ask": "Drop anything social. Lean into CTV." },
{ "scope": "product", "id": "prod_low_quality", "action": "omit" }
],
"maxProducts": 8
}
```
The response includes the underlying `discoveryId` so you can drop into the
manual flow at any point to inspect or adjust the selection.
## Best practices
* Lead with the **outcome**, not just the demographic. Agents score against
campaign objective + creative + audience together.
* Include guardrails that matter: brand-safety needs, format
constraints (`16:9`, `:30s`), exclusions.
* Keep briefs under \~500 characters when possible — long briefs are
auto-summarized for LLM enrichment but lose nuance.
* The first `discover-products` call does the heavy work (LLM
enrichment, agent fan-out). Subsequent `GET /discover-products` calls
hit per-group cache and are cheap.
* Cache TTL is 4 hours. After that, re-run `POST /discover-products`
with the same brief to refresh.
* Pre-warm extra pages by paging through `groupOffset` immediately —
results stay in cache for the rest of the session.
* Treat per-source credentials (API keys, OAuth tokens) as secrets —
never log them, never embed them in client bundles. Apostra stores
them encrypted at rest and redacts them from server logs.
* Rotate per the source's policy. Discovery results are cached per
`(account, source-credential-fingerprint)` so private results never
leak across buyers.
* Public-only discovery is always available without credentials — fall
back gracefully if the buyer hasn't registered any.
* Combine scopes in one `refine` array. The seller responds per
instruction in `refinementApplied`.
* `more_like_this` works best with a clear `ask` (audience, format,
pricing band).
* Use `proposal.action: "finalize"` when you're committing — it asks
the seller for firm pricing and an inventory hold.
## Related
* [Campaigns guide](/v2/object-guides/campaign) — promote a discovery selection into a live media buy
* [Storefronts](/v2/object-guides/storefront) — connect, refresh, and manage credentials per inventory source
* [Buyer storefronts](/v2/setup/buyer-onboarding) — register against operator-hosted storefronts
* [Buyer API reference](/v2/buyer-api-reference) — full endpoint and schema reference
# Frequency Caps
Source: https://docs.apostra.com/v2/guides/frequency-caps
Limit how often a single user is exposed to your ads across publishers
## Overview
A **frequency cap** limits how many times a single user can be exposed to your
advertising within a rolling time window. Caps are enforced by Apostra across
every publisher running a given advertiser, campaign, or creative — so a user
who sees the cap-worth of impressions on one publisher will not see another
impression on a different publisher in the same window.
Apostra stores caps as **buyer-side** configuration. They are distinct from any
`frequency_cap` a publisher may apply on its own inventory and travel with the
buyer's request as part of the targeting overlay.
Buyer-side caps are about *your* exposure budget for a user — they are
enforced even when you buy across publishers that have no view of each
other's delivery.
Cross-publisher enforcement on a given seller's inventory requires that seller
to be connected to real-time targeting — see
[Real-time targeting (TMP)](/v2/storefront/tmp/overview). **On inventory
that isn't TMP-connected — Meta, TikTok, Snap, and other closed-platform
integrations — this cap is never enforced there, not delayed and not
partially applied.** These sellers run their own ad server end-to-end, so
Apostra never gets a request-time moment to check the shared counter this
cap relies on. Use each platform's own native cap instead
(`targeting_overlay.frequency_cap` on the package — a different field from
the one on this page) for that inventory; see the
[ad platform compatibility guides](/v2/guides/ad-platform-compatibility/index)
for what each platform's native cap supports.
**Closed-platform publishers (TikTok, Meta, Snap, Pinterest, Reddit, Spotify,
Amazon, Google, LinkedIn, and other platform-adapter integrations) can never
be TMP-connected.** These platforms run their own ad server and auction
end-to-end — Apostra talks to their campaign-management API, not a
real-time bidstream, so there is no request-time moment for a `frequencyCaps`
cap to act on. This isn't a setup gap that can be fixed later; it's a
structural property of how these platforms are integrated.
If you want a hard, per-publisher frequency cap on one of these platforms —
e.g. TikTok's own native ad-group frequency setting — set `frequency_cap` in
the **product-level** `targetingOverlay` when you add the product
(`add_discovery_products` / `create_media_buys`), not the campaign-level
`frequencyCaps` field described on this page. The two are unrelated fields:
`frequencyCaps` is Apostra's own buyer-side exposure budget, and
`targetingOverlay.frequency_cap` is the publisher-side setting that ships in
the request the publisher actually receives.
**`targetingOverlay.frequency_cap` isn't uniformly enforced across
publishers either — check before relying on it as a guarantee.** A simple
cap (a fixed count per day, for example) works correctly on Meta, TikTok,
and Snap today. An out-of-shape request — a whole-flight cap
(`window.unit: 'campaign'`), for instance — is handled three different
ways: TikTok and Snap reject it outright with a clear error, but **Meta
accepts the request and silently never applies the cap**, with no error and
no indication anywhere in the response. Snap additionally only honors a cap
for a subset of optimization goals, and requires `per: "individuals"` set
explicitly.
## Concept
Every cap config is a triple of:
| Field | Description |
| - | - |
| `targetLevel` | What entity the cap applies to: `ADVERTISER`, `CAMPAIGN`, or `CREATIVE` |
| `max_impressions` | Maximum number of impressions allowed in the window |
| `window` | Rolling window expressed as an AdCP `Duration` (`{ interval, unit }`) |
`window.unit` accepts these values:
| Unit | Use it for |
| - | - |
| `minutes` | Minute-level pacing |
| `hours` | "No more than 2 impressions per hour" |
| `days` | "No more than 5 impressions per day" |
| `campaign` | Lifetime cap for the campaign |
`max_impressions` is a positive integer.
### Buyer-side vs publisher-side
The schema deliberately omits the publisher-side fields from the AdCP
`FrequencyCap` shape (`suppress`, `suppress_minutes`, `per`). Those are
suppression instructions that publishers act on inside their own ad server.
Buyer-side caps care only about how many impressions you'll spend on a single
user — Apostra picks the reach unit, not you.
### Cascade across levels
Caps are applied at the level you set them on:
* **Advertiser-level** caps apply to every campaign on the advertiser.
* **Campaign-level** caps apply to every media buy and creative inside that
campaign.
* **Creative-level** caps apply to that single creative across every campaign
it runs in.
When more than one cap matches a request, **all** caps must be satisfied for
the impression to serve. The strictest cap wins.
## How to use
Frequency caps are not a standalone REST resource — they are embedded into the
parent advertiser, campaign, or creative endpoints. To change the caps on an
entity, send the full array you want stored. **PUT replaces the full array.**
### On campaign create
```bash theme={null}
curl -X POST 'https://api.apostra.com/api/v2/buyer/campaigns' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"advertiserId": "12345",
"name": "Spring Launch",
"flightDates": { "startDate": "2026-05-01T00:00:00Z", "endDate": "2026-05-31T23:59:59Z" },
"budget": { "total": 50000, "currency": "USD" },
"frequencyCaps": [
{ "max_impressions": 3, "window": { "interval": 1, "unit": "days" } },
{ "max_impressions": 10, "window": { "interval": 7, "unit": "days" } }
]
}'
```
This sets two caps that both apply: a user sees at most 3 impressions per day
**and** at most 10 impressions per week.
### On campaign update
```bash theme={null}
curl -X PUT 'https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"frequencyCaps": [
{ "max_impressions": 2, "window": { "interval": 12, "unit": "hours" } }
]
}'
```
Sending `frequencyCaps` on update **replaces** all previously stored caps
for that target. To clear caps entirely, send an empty array. Omit the
field to leave existing caps untouched.
### On the advertiser
```bash theme={null}
curl -X PUT 'https://api.apostra.com/api/v2/buyer/advertisers/12345' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"frequencyCaps": [
{ "max_impressions": 20, "window": { "interval": 7, "unit": "days" } }
]
}'
```
This puts a brand-wide ceiling: no user sees more than 20 impressions for
this advertiser in any 7-day window, regardless of which campaign serves
them.
### On a creative
```bash theme={null}
curl -X PUT 'https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/creatives/cm_abcdef' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"frequencyCaps": [
{ "max_impressions": 1, "window": { "interval": 1, "unit": "days" } }
]
}'
```
Useful for "first-impression" creatives that should run at most once per user
per day.
## Response shape
`GET` responses on advertiser, campaign, or creative include the caps with
their full server-assigned metadata:
```json theme={null}
{
"frequencyCaps": [
{
"id": "12345",
"targetLevel": "CAMPAIGN",
"targetId": "cmp_987654321",
"max_impressions": 3,
"window": { "interval": 1, "unit": "days" },
"createdAt": "2026-04-25T10:30:00.000Z",
"updatedAt": "2026-04-25T10:30:00.000Z",
"archivedAt": null
}
]
}
```
## Behavior
`replaceForTarget` archives every existing cap for the
`(customer_id, target_level, target_id)` triple, then inserts the new set
in a single transaction. There is no partial update.
Old caps stay in the database with `archived_at` set. They are excluded
from API responses but preserved for audit and reporting.
When multiple caps apply to a request, the impression serves only if every
cap allows it. There is no priority or override.
A `CAMPAIGN` cap counts impressions on that campaign only. An `ADVERTISER`
cap counts impressions across every campaign on that advertiser.
## Best practices
* **Layer caps from broad to narrow.** Set an advertiser-level lifetime cap
for brand exposure, then add tighter campaign-level caps for short flights.
* **Keep windows in human units.** `{ interval: 1, unit: "days" }` is easier
to reason about than `{ interval: 1440, unit: "minutes" }`.
* **Use `campaign` unit for lifetime caps.** Don't try to express "for the
entire campaign" as a multi-week `days` window — use `unit: "campaign"`.
* **Empty array clears caps.** To remove all caps from a target, send
`"frequencyCaps": []`. Omitting the field leaves them in place.
* **Don't fight the publisher.** Publisher-side suppression in
`target_overlay` is a separate feature; if you need to enforce that a user
doesn't see your ad in a given session, work with the agent rather than
layering more buyer-side caps.
## Limits
| Limit | Value |
| - | - |
| Levels available | `ADVERTISER`, `CAMPAIGN`, `CREATIVE` |
| Window units | `minutes`, `hours`, `days`, `campaign` |
| `max_impressions` | Positive integer |
| Caps per target | No hard limit, but each cap layers — keep it small |
# Log Events
Source: https://docs.apostra.com/v2/guides/log-events
Send custom events to an advertiser for downstream measurement and reporting
## Overview
`POST /api/v2/buyer/advertisers/:advertiserId/log-event` is a single-call ingest endpoint for sending outcome or marketing events to an advertiser. It accepts the standard ADCP event types — `purchase`, `refund`, `lead`, `add_to_cart`, `initiate_checkout`, `view_content`, `content_view`, `watch_milestone`, `follow`, `complete_registration`, `subscribe`, `start_trial`, `page_view`, `app_install`, `app_launch` — and a `custom` event type for events that don't map to any of those.
Use it for:
* **Bespoke conversion definitions** — a "qualified demo booked" or "tier upgrade" event that doesn't fit the standard taxonomy. Send `event_type: custom` with `custom_event_name`.
* **Internal-only events for ML training** — engagement signals you want to feed measurement, even if they aren't reportable conversions.
* **Custom funnel steps** — domain-specific milestones (game level reached, KYC step completed) that drive your bidding logic.
* **Server-to-server batch ingestion** — up to 10,000 events per call.
## How it differs from event sources and measurement data
The three measurement ingest paths overlap; pick the one that matches the shape of your data.
| Surface | What you send | Granularity | Best for |
| - | - | - | - |
| **Event sources** (`/event-sources/sync`) | The *configuration* of where events come from (pixels, SDKs, MMPs) | Source registration | Onboarding a new pixel or feed once, then sending events through it |
| **Log events** (`/log-event`) | Per-user, per-event records keyed to a registered `event_source_id` | One record per event (1–10,000 per call) | Bespoke conversion taxonomies, custom funnel steps, server-to-server event ingestion |
| **Measurement data** (`/measurement-data/sync`) | Pre-aggregated outcomes for a time window and entity | One record per metric × window × campaign | MMP, CRM, or warehouse exports of weekly/daily aggregates |
`log-event` is the path for **identified, per-user events at advertiser scope**. Event sources are the *configuration layer* you call once; `log-event` is the runtime path you call repeatedly.
Every event must reference an `event_source_id` that is already registered
on the advertiser via `POST /advertisers/:advertiserId/event-sources/sync`.
Events sent against an unregistered source are rejected. See the
[Measurement & Incrementality guide](/v2/guides/measurement-incrementality)
for event-source setup.
## Request shape
All examples use:
```
BASE = https://api.apostra.com/api/v2/buyer
AUTH = Authorization: Bearer scope3_
```
```bash theme={null}
curl -X POST "$BASE/advertisers/12345/log-event" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"event_source_id": "website_pixel",
"events": [
{
"event_id": "txn_abc123",
"event_type": "purchase",
"event_time": "2026-04-26T14:30:00-05:00",
"action_source": "website",
"event_source_url": "https://shop.example.com/checkout/complete",
"user_match": {
"hashed_email": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
"click_id": "fb.1.1700000000.AbCdEfGh",
"click_id_type": "fbclid"
},
"custom_data": {
"value": 89.99,
"currency": "USD",
"order_id": "order_55512",
"content_ids": ["sku_red_shoe_42"],
"num_items": 1
}
},
{
"event_id": "demo_booked_98765",
"event_type": "custom",
"custom_event_name": "qualified_demo_booked",
"event_time": "2026-04-26T15:05:00-05:00",
"action_source": "website",
"user_match": {
"hashed_email": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
}
]
}'
```
```json Response theme={null}
{
"events_received": 2,
"events_processed": 2,
"warnings": [],
"match_quality": 0.87
}
```
### Top-level fields
| Field | Type | Required | Notes |
| - | - | - | - |
| `event_source_id` | string | yes | Must match an `event_source_id` registered on the advertiser |
| `events` | array | yes | 1–10,000 events per call |
| `test_event_code` | string | no | Marks the request as test traffic — events are validated but excluded from production data |
### Per-event fields
| Field | Type | Required | Notes |
| - | - | - | - |
| `event_id` | string | yes | Unique within `event_type` + `event_source_id`. Resends with the same `event_id` are deduplicated. |
| `event_type` | enum | yes | Canonical ADCP event type such as `purchase`, `lead`, `add_to_cart`, `initiate_checkout`, `view_content`, `content_view`, `watch_milestone`, `follow`, `complete_registration`, `page_view`, `app_install`, `subscribe`, `start_trial`, `refund`, or `custom` |
| `event_time` | ISO 8601 | yes | Must include offset (e.g. `2026-04-26T14:30:00-05:00`) |
| `custom_event_name` | string | conditional | Required when `event_type: custom` |
| `action_source` | enum | no | `website`, `app`, `in_store`, `phone_call`, `system_generated`, `other` |
| `event_source_url` | URL | conditional | Required when `action_source: website` |
| `user_match` | object | no | Identity fields for attribution (see below) |
| `custom_data` | object | no | Value, currency, order ID, items |
### `user_match` — identity fields
Send at least one identifier per event. More identifiers = higher match rate.
| Field | Format | Notes |
| - | - | - |
| `uids` | array of `{ type, value }` | Universal IDs: `rampid`, `id5`, `uid2`, `euid`, `pairid`, `maid` |
| `hashed_email` | 64-char lowercase hex | SHA-256 of lowercased, trimmed email |
| `hashed_phone` | 64-char lowercase hex | SHA-256 of E.164-formatted phone |
| `click_id` + `click_id_type` | string | Platform click identifier (`fbclid`, `gclid`, `ttclid`) |
| `client_ip` | string | IP for probabilistic matching |
| `client_user_agent` | string | UA for probabilistic matching |
### `custom_data` — event payload
| Field | Type | Notes |
| - | - | - |
| `value` | number | Monetary value of the event |
| `currency` | string | ISO 4217 (`USD`, `EUR`, `GBP`) — required when `value` is set |
| `order_id` | string | Unique transaction ID |
| `content_ids` | array of strings | SKU / product IDs |
| `content_type` | string | Category (`product`, `service`) |
| `num_items` | integer | Item count |
| `contents` | array of `{ id, quantity, price, brand }` | Per-item detail |
### Response shape
| Field | Type | Notes |
| - | - | - |
| `events_received` | integer | Count of events in the request |
| `events_processed` | integer | Count successfully queued |
| `partial_failures` | array | Per-event failures with `event_id`, `code`, `message` |
| `warnings` | array of strings | Non-fatal issues — low match quality, missing optional fields |
| `match_quality` | number (0–1) | Aggregate match-quality score for the batch |
A `200` with `partial_failures` is normal — bad events are rejected individually, the rest succeed. Treat any failure as something to fix at source, not a transient error.
## Best practices
Custom event names become the join key for downstream measurement and
reporting. Pick names you won't want to change (`qualified_demo_booked`,
`kyc_step_2_complete`). Renaming after launch breaks historical
continuity.
If your event is genuinely a purchase or a lead, use `purchase` or
`lead`. Standard types get first-class treatment in measurement and
optimization; `custom` events fall back to generic handling.
`event_id` is the dedup key, scoped to `event_type` + `event_source_id`.
Re-sending the same `event_id` is safe — duplicates are dropped. Use
your stable upstream identifier (transaction ID, demo booking ID) so
retries don't double-count.
Recent events match better — click IDs and probabilistic signals decay
quickly. Aim for sub-hour latency for behavioral events; daily batches
are fine for offline conversions like CRM exports.
Set `test_event_code` for test traffic. Events validate end-to-end
but are excluded from production reporting and the measurement engine.
The endpoint accepts up to 10,000 events per call. Batch larger flows
rather than firing one HTTP request per event — same data, much less
overhead.
## Privacy
**Never send raw PII.** No plaintext emails, phone numbers, names, or
addresses. Only pre-hashed identifiers (SHA-256, lowercase, 64-char hex)
or pre-resolved identity tokens (RampID, UID2, ID5, etc.). Normalize the
email (lowercase, trimmed) and phone (E.164) **before** hashing — see the
[Conversion API guide](/v2/integrations/conversion-api) for hashing
rules. Malformed hashes are silently dropped.
The same privacy contract applies whether an event is `purchase` or `custom`. Custom events are not a back door for raw PII.
## Related
Detailed identity-hashing rules and the same `log-event` payload viewed
as a conversion-tracking integration.
Register event sources, configure measurement, and run incrementality
tests against the events you log.
# Measurement & Incrementality
Source: https://docs.apostra.com/v2/guides/measurement-incrementality
Configure event sources, ingest measurement data, and run incrementality tests with the v2 measurement engine
## Overview
Apostra Measurement Engine turns raw advertising signals into causal evidence about what is working. Instead of treating every conversion as proof, it asks the harder question — *would that conversion have happened anyway?* — and updates a running set of beliefs about each campaign's incremental impact as new data arrives.
The pipeline has four layers:
1. **Event sources** — buyer-registered pixels, SDKs, MMPs, CRMs, or measurement partners that send conversions, impressions, or outcome data.
2. **Measurement data** — privacy-safe outcome records (revenue, conversions, LTV) attached to a campaign, media buy, package, or creative.
3. **Belief state** — a Bayesian summary of what the engine currently believes about each hypothesis (e.g. "audience segment A drives more incremental revenue than segment B"), expressed as posterior distributions with confidence intervals.
4. **Incrementality tests** — explicit treatment / control / observation cohorts and test plans that produce stronger causal estimates than passive observation.
**How belief updating works (high level)**
The engine starts each hypothesis with a prior — your initial guess about
size and confidence. As measurement records arrive, the learning cycle
updates that prior into a posterior using Bayesian inference: high-quality,
fresh data shifts beliefs faster; sparse or noisy data shifts them less.
Running an A/B test with proper test and control cohorts produces the
strongest evidence and tightens the posterior fastest.
**Privacy-first**: the measurement engine never accepts raw PII (plain emails,
phone numbers, addresses). Send pre-hashed identifiers (SHA-256) or
pre-resolved identity tokens (RampID, UID2, ID5, etc.) — see the
[Conversion API guide](/v2/integrations/conversion-api) for hashing rules.
## How evidence stays with the advertiser
Measurement sources, records, hypotheses, test plans, learning records, and
belief state are stored under the advertiser scope. They are not separate
seller-owned histories. When a measured campaign uses more than one seller,
authorized users and agents can read its submitted outcomes from the same
advertiser-scoped endpoints and use that evidence when planning later work
across sellers.
This evidence exists only when measurement is configured and data is submitted.
A seller relationship or campaign by itself does not create outcome evidence,
and seller delivery data is available only when the seller returns it.
Advertiser-scoped evidence is durable product state, not private model memory
carried by an individual agent. Access follows the customer and advertiser
permissions on every request. Changing sellers does not move the records into
a seller account or grant that seller access. Belief state remains read-only:
new data, a new test, human feedback, or a new hypothesis is required before a
later learning cycle can change it.
## Prerequisites
Generate a key at [app.apostra.com/user-api-keys](https://app.apostra.com/user-api-keys).
See [Authentication](/v2/authentication) for setup.
All measurement endpoints are scoped to an advertiser. You'll use this in
every URL: `/api/v2/buyer/advertisers/:advertiserId/...`.
Measurement data is most useful when attached to in-flight campaigns.
See [Campaigns](/v2/object-guides/campaign) for setup.
All examples below use:
```
BASE = https://api.apostra.com/api/v2/buyer
AUTH = Authorization: Bearer scope3_
```
## Step 1: Register Event Sources
An **event source** is a logical channel through which measurement events flow — a website pixel, a mobile SDK, a CRM export, or an MMP feed. Every event you send must reference a registered `event_source_id`.
Use the ADCP-spec sync endpoint to upsert event sources for an advertiser:
```bash cURL theme={null}
curl -X POST "$BASE/advertisers/12345/event-sources/sync" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"account": { "account_id": "12345" },
"event_sources": [
{
"event_source_id": "website_pixel",
"name": "Website Pixel",
"event_types": ["purchase", "add_to_cart", "lead"],
"allowed_domains": ["shop.example.com", "checkout.example.com"],
"example_event": {
"event_type": "purchase",
"hashed_email": "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
"value": 89.99,
"currency": "USD"
}
},
{
"event_source_id": "crm_import",
"name": "Salesforce CRM Export",
"event_types": ["purchase", "qualify_lead"],
"allowed_domains": []
}
],
"delete_missing": false
}'
```
Identifiers like `hashed_email` and `hashed_phone` MUST be SHA-256 hex
(lowercase, 64 chars). Normalize email to lowercase / trimmed and phone to
E.164 *before* hashing — see the
[Conversion API guide](/v2/integrations/conversion-api) for full hashing
rules. The engine silently drops malformed hashes.
Leaving `allowed_domains` empty (or omitting it) means events from **any
origin** will be accepted for that source. Always restrict `allowed_domains`
to the hosts that legitimately fire your pixel; an empty list is appropriate
only for server-to-server feeds (CRM, MMP) where there is no browser origin.
```json Response theme={null}
{
"event_sources": [
{
"event_source_id": "website_pixel",
"action": "created",
"setup": {
"snippet_type": "server_only",
"instructions": "Events are ingested server-to-server. Send accepted events to POST /api/v2/advertisers/12345/log-event using your API credentials. Setup guide: https://docs.apostra.com/v2/guides/log-events"
}
},
{ "event_source_id": "crm_import", "action": "created" }
]
}
```
Live results (`created`, `updated`, `unchanged`) carry a `setup` object so
install guidance travels with the response, not only inside the widget.
Apostra ingests every source server-to-server via `log_event`, so
`snippet_type` is always `server_only` and no client-side `snippet` is
returned; `instructions` names the ingestion endpoint and links the setup
guide. `failed` and `deleted` results omit `setup`.
### Request fields
| Field | Type | Required | Description |
| - | - | - | - |
| `account.account_id` | string | yes | Must match the `:advertiserId` in the URL |
| `event_sources[].event_source_id` | string | yes | Buyer-assigned ID, max 255 chars |
| `event_sources[].name` | string | no | Human-readable label |
| `event_sources[].event_types` | string\[] | no | Restricts which event types this source may send. Omit to accept all. |
| `event_sources[].allowed_domains` | string\[] | no | Origin domains authorized for this source |
| `delete_missing` | boolean | no | Archive any buyer-managed sources not in this payload (default `false`) |
Each result's `action` is one of `created`, `updated`, `unchanged`, `failed`, or `deleted`.
### List configured sources
```bash theme={null}
curl "$BASE/advertisers/12345/event-sources?take=50&skip=0" \
-H "Authorization: Bearer scope3_"
```
Events sent to an unregistered `event_source_id` are rejected. Always sync
sources before turning on a pixel or CRM job.
### How a source reaches each seller account
You define an event source once, for the advertiser. On a platform where you
own the ad account, such as Meta or Snap, the seller optimizes against a pixel
that already exists on that account: a Meta dataset or a Snap Pixel. Apostra
binds your event source to that pixel separately on each seller account. It
never creates a pixel on the account and never sends your events while binding.
Binding at media buy creation, and sending the bound pixel on the media buy,
are rolling out account by account. Until they are enabled for your account,
creating a media buy does not bind your event sources and sends your
`event_source_id` to the seller unchanged.
Binding starts when you create a media buy whose optimization goal names the
event source. For each seller account the buy runs on, Apostra lists the
pixels on that account and picks one:
1. If you named a pixel for that account, Apostra binds it. If that pixel is
not on the account, the account reports
`SELLER_EVENT_SOURCE_DESTINATION_MISSING`; Apostra never binds a different
one instead.
2. If you named none and the account has exactly one pixel, Apostra binds it.
3. If you named none and the account has several, the account reports
`SELLER_EVENT_SOURCE_DESTINATION_AMBIGUOUS`. Name the pixel to use.
4. If the account has no pixel, the account reports
`SELLER_EVENT_SOURCE_DESTINATION_MISSING`.
To name the pixel, save the source with `sellerAccounts` through
[`save_event_source`](/v2/setup/v3/tool-reference). Binding happens only while
the **Conversion events** switch is on for that seller in Connections. While
it is off, the account reports `SELLER_DATA_SHARING_NOT_PERMITTED` and nothing
is sent to the seller; turn the switch on and Apostra binds the account within
about an hour. Read `sellerAccounts` on the event source to see each account's
bound pixel (`sellerId`), its `status`, and any blocker. A seller whose
platform cannot bind event sources reports `SELLER_OBJECT_TYPE_UNSUPPORTED`.
Once an account is bound, later media buys on that account use the same
pixel without binding again.
#### What the media buy sends
The platforms accept only their own pixel ID in a conversion goal. So when a
media buy's optimization goal names one of your event sources, the media buy
Apostra creates on Snap, Meta or TikTok carries the pixel bound on that
seller account instead of your `event_source_id`. Your media buy keeps
showing your own `event_source_id`; only the request sent to the seller
changes. Event sources the seller manages, and the platform's own built-in
events, are sent unchanged.
Where the binding stands on the seller account decides what happens to a new
media buy:
| Binding on the seller account | What happens to the media buy | What to do |
| - | - | - |
| Bound | Sent with the bound pixel. | Nothing. |
| Still being bound | Not sent yet. Launching reports `SELLER_OBJECT_SYNC_PENDING` for the buy, which stays a draft. Apostra checks again about every 30 seconds and sends the buy once the pixel is bound. Waiting does not use up the buy's retries. | Nothing. |
| `SELLER_EVENT_SOURCE_DESTINATION_MISSING` | Fails with that code. | Create the pixel in the platform's own console, or name a pixel that is on the account with `sellerAccounts`. Then create a new media buy. |
| `SELLER_EVENT_SOURCE_DESTINATION_AMBIGUOUS` | Fails with that code. This also happens when two of your connected seller accounts for the same ad account have different pixels bound. | Name the pixel to use with `sellerAccounts`. Then create a new media buy. |
| `SELLER_DATA_SHARING_NOT_PERMITTED` | Fails with that code. | Turn on **Conversion events** for that seller in Connections. Then create a new media buy. |
| `SELLER_OBJECT_TYPE_UNSUPPORTED` | Fails with that code. The seller cannot use your event sources: a managed seller agent, an AdCP agent outside Apostra, a platform that cannot bind them, or an ad account that is not one of your connected seller accounts. | Optimize the buy toward a metric instead, or buy from a seller account that can bind your event sources. |
| `SELLER_OBJECT_SYNC_FAILED`, `SELLER_OBJECT_SYNC_EXPIRED` or `SELLER_OBJECT_SYNC_RECONCILIATION_REQUIRED` | Fails with that code: Apostra could not confirm a pixel on the account. | Submit a new media buy or contact support. |
A failed media buy shows the code at the start of its failure reason. On a
seller that approves media buys by hand, the buy waits or fails before it
reaches the seller's approval queue, and the approved buy is sent with the
bound pixel.
What you still need to do yourself:
* Create the pixel or dataset in the platform's own console if the account has
none, and install it on your site or app so it receives events. Binding
connects to an existing pixel; it does not install one.
* Grant Apostra access to the pixel or dataset when you connect the account
(see [Connecting ad platforms](/v2/guides/connecting-ad-platforms)).
* Keep sending your conversion events to Apostra through
[`log_event`](/v2/guides/log-events). Binding records which pixel your goal
uses on each account; it does not forward events to the seller.
## Step 2: Configure Measurement
Measurement configuration controls which measurement features are active for an advertiser — Marketing Mix Modeling (MMM), incrementality testing, and brand lift — plus any provider-specific MMM settings.
```bash theme={null}
curl -X PUT "$BASE/advertisers/12345/measurement-config" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"mmmEnabled": true,
"mmmConfig": {
"provider": "measured",
"dataSourceIds": ["ds_revenue", "ds_orders"],
"reportingFrequency": "weekly"
},
"brandLiftEnabled": false,
"settings": {
"defaultLookbackDays": 28
}
}'
```
### Fields
| Field | Type | Description |
| - | - | - |
| `mmmEnabled` | boolean | Enable Marketing Mix Modeling |
| `mmmConfig.provider` | string | MMM partner name (e.g. `measured`) |
| `mmmConfig.dataSourceIds` | string\[] | IDs of upstream data feeds powering MMM |
| `mmmConfig.reportingFrequency` | enum | `weekly`, `monthly`, or `quarterly` |
| `brandLiftEnabled` | boolean | Enable brand-lift study integration |
| `settings` | object | Free-form key/value advertiser-level overrides |
`PUT` is upsert — pass only the fields you want to set; missing fields fall back to defaults / prior values. Read the current config with `GET /advertisers/:advertiserId/measurement-config`.
## Step 3: Validate Configuration
Before relying on incrementality estimates, assess whether your planned spend, geos, and flight length can actually move the needle on a hypothesis. The testability assessment returns power-analysis-style guidance.
```bash theme={null}
curl -X POST "$BASE/advertisers/12345/testability" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"hypothesisIds": ["1f5c0c4e-2c9f-4f0a-9e31-2d4f3c7b1a01"],
"budget": 250000,
"weeklyBudget": 25000,
"flightWeeks": 10,
"testGeos": ["US-CA", "US-OR", "US-WA"],
"controlGeos": ["US-AZ", "US-NV", "US-NM"]
}'
```
The response surfaces, per hypothesis, whether the design has enough cells, whether geos are well-matched, and which gaps would weaken inference.
Run testability *before* launching media. It's far cheaper to widen your geo
list or extend the flight than to discover post-flight that the test was
underpowered.
## Step 4: Send Measurement Data
There are two complementary ways to feed the engine:
* **Conversion events** — fine-grained, per-user actions (purchases, leads, sign-ups). Use the [Conversion API](/v2/integrations/conversion-api) — same identity rules apply.
* **Measurement records** — pre-aggregated outcomes for a time window and geo (e.g. "incremental revenue, US-CA, week of 2026-03-01 = \$8,450"). Use the sync endpoint below.
### Sync aggregated measurement data
```bash theme={null}
curl -X POST "$BASE/advertisers/12345/measurement-data/sync" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"measurements": [
{
"start_time": "2026-03-01T00:00:00-05:00",
"end_time": "2026-03-07T23:59:59-05:00",
"metric_id": "incremental_revenue",
"metric_value": 8450.75,
"unit": "currency",
"currency": "USD",
"campaign_id": "camp_456",
"media_buy_id": "mb_789",
"source": "advertiser",
"source_platform": "billy_grace",
"source_metric_name": "Incremental Revenue",
"external_row_id": "bg_row_001"
},
{
"start_time": "2026-03-01T00:00:00-05:00",
"end_time": "2026-03-07T23:59:59-05:00",
"metric_id": "purchase_count",
"metric_value": 142,
"unit": "count",
"campaign_id": "camp_456",
"external_row_id": "bg_row_002"
}
]
}'
```
| Field | Type | Required | Notes |
| - | - | - | - |
| `start_time` / `end_time` | ISO 8601 | yes | Must include offset; `start_time < end_time` |
| `metric_id` | enum | yes | `revenue`, `incremental_revenue`, `conversions`, `incremental_conversions`, `page_view_count`, `add_to_cart_count`, `purchase_count`, `ltv_1d`, `ltv_7d`, `ltv_30d` |
| `metric_value` | number | yes | The measured value |
| `unit` | enum | yes | `currency`, `count`, `ratio`, `percentage` |
| `currency` | string | conditional | ISO 4217 — required when `unit` is `currency` |
| `campaign_id` / `media_buy_id` / `package_id` / `creative_id` | string | one required | Attaches the measurement to an entity |
| `source` | enum | no | `advertiser`, `mmp`, or `measurement_partner` |
| `external_row_id` | string | no | Idempotency key for re-syncs |
Up to **1,000 measurements per call**. Each result reports `action: created | updated | unchanged | failed`.
### Upload raw measurement records (advanced)
For research-style flows that already produce per-geo outcomes, the learning engine accepts batched records directly:
```bash theme={null}
curl -X POST "$BASE/advertisers/12345/measurement-records" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"records": [
{
"outcomeType": "incremental_revenue",
"geo": "US-CA",
"timeWindowStart": "2026-03-01",
"timeWindowEnd": "2026-03-07",
"value": 8450.75,
"baselineValue": 7100.00,
"confidenceInterval": 0.92,
"source": "measured",
"lagDays": 7
}
]
}'
```
Up to **5,000 records per call**.
### Upload context records
Context records describe market conditions that the learning engine should partial out — promos, weather, competitor activity, seasonality:
```bash theme={null}
curl -X POST "$BASE/advertisers/12345/context-records" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"records": [
{
"geo": "US-CA",
"timeWindowStart": "2026-03-01",
"timeWindowEnd": "2026-03-07",
"promoActive": true,
"promoType": "site_wide_15_off",
"seasonalityIndex": 1.12,
"flightStatus": "active"
}
]
}'
```
Never include raw user identifiers (emails, phone numbers, names) in
measurement or context records. These endpoints accept aggregated outcomes
only — per-user events go through the [Conversion API](/v2/integrations/conversion-api).
## Step 5: Inspect the Event Summary
Once events are flowing, the event-summary endpoint returns hourly counts per event type so you can confirm ingestion before depending on downstream attribution:
```bash theme={null}
curl "$BASE/advertisers/12345/events/summary?\
eventType=conversion&\
startHour=2026-03-27T14:00:00Z&\
endHour=2026-03-27T20:00:00Z" \
-H "Authorization: Bearer scope3_"
```
```json Response theme={null}
{
"periodStart": "2026-03-27T14:00:00.000Z",
"periodEnd": "2026-03-27T20:00:00.000Z",
"entries": [
{ "eventHour": "2026-03-27T14:00:00.000Z", "eventType": "conversion", "eventCount": 1500 },
{ "eventHour": "2026-03-27T15:00:00.000Z", "eventType": "conversion", "eventCount": 1612 },
{ "eventHour": "2026-03-27T16:00:00.000Z", "eventType": "conversion", "eventCount": 1483 }
],
"totalEventCount": 4595
}
```
| Query param | Notes |
| - | - |
| `eventType` | One of `conversion`, `click`, `impression`, `measurement`, `mmp`. Omit for all types. |
| `startHour` / `endHour` | Hour-aligned ISO 8601 timestamps. Defaults to the last completed UTC hour. |
You can also check measurement freshness — gaps in expected geo/time coverage:
```bash theme={null}
curl "$BASE/advertisers/12345/measurement-freshness?\
flightStart=2026-03-01&\
flightEnd=2026-04-30&\
geos=US-CA,US-OR,US-WA" \
-H "Authorization: Bearer scope3_"
```
## Step 6: Set Up Test Cohorts and Test Plans
Incrementality tests work by comparing well-defined groups. The v2 model has three layers:
* **Hypotheses** — *what* you're testing: a falsifiable claim about a treatment vs. a comparison ("premium CTV drives more incremental revenue per impression than general CTV"). Hypotheses are persistent — each test plan accumulates evidence against the same hypothesis over time.
* **Test cohorts** — *who* is in each arm (treatment, control, observation). A cohort is a flexible audience definition (geo, segment, zip code, custom).
* **Test plans** — *how* the comparison runs against a hypothesis: which conditions, which dimensions to match on, how cells are sized.
### Create a hypothesis
Every test plan is anchored to a hypothesis. Create one before linking test plans, cohorts, or allocation entries to it.
```bash theme={null}
curl -X POST "$BASE/advertisers/12345/hypotheses" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"category": "channel",
"statement": "Premium CTV inventory drives more incremental purchases than general CTV at the same CPM.",
"treatment": "Premium CTV (top-tier publisher list)",
"comparison": "General CTV (open marketplace)",
"outcomeMetric": "incremental_revenue",
"icon": "📺",
"priorConfidence": 0.4,
"priorMagnitude": 0.15,
"incrementalCostCpm": 8.0,
"measurementSource": "billy_grace",
"measurementLagWeeks": 1,
"minimumTestCells": 6,
"proxyMetrics": [
{ "outcomeType": "add_to_cart_count", "weight": 0.3 },
{ "outcomeType": "purchase_count", "weight": 0.7 }
]
}'
```
```json Response theme={null}
{
"id": "1f5c0c4e-2c9f-4f0a-9e31-2d4f3c7b1a01",
"advertiserId": "12345",
"category": "channel",
"statement": "Premium CTV inventory drives more incremental purchases than general CTV at the same CPM.",
"treatment": "Premium CTV (top-tier publisher list)",
"comparison": "General CTV (open marketplace)",
"outcomeMetric": "incremental_revenue",
"icon": "📺",
"priorConfidence": 0.4,
"currentConfidence": 0.4,
"priorMagnitude": 0.15,
"currentMagnitude": 0.15,
"incrementalCostCpm": 8.0,
"measurementSource": "billy_grace",
"measurementLagWeeks": 1,
"minimumTestCells": 6,
"proxyMetrics": [
{ "outcomeType": "add_to_cart_count", "weight": 0.3 },
{ "outcomeType": "purchase_count", "weight": 0.7 }
],
"groundTruth": null,
"status": "no_buys",
"confidenceHistory": [],
"createdBy": "user_98765",
"sourceName": null,
"createdAt": "2026-04-26T16:00:00.000Z",
"updatedAt": "2026-04-26T16:00:00.000Z"
}
```
| Field | Type | Required | Notes |
| - | - | - | - |
| `category` | enum | yes | `audience`, `creative`, `channel`, `context`, `timing`, `tactic` |
| `statement` | string | yes | The falsifiable claim being tested |
| `treatment` | string | yes | Description of the treatment arm |
| `comparison` | string | yes | Description of the comparison / control arm |
| `outcomeMetric` | string | yes | Primary metric (e.g. `incremental_revenue`, `purchase_count`) |
| `priorConfidence` | number (0–1) | yes | Initial confidence before any data |
| `priorMagnitude` | number | yes | Expected effect size before any data |
| `icon` | string | no | Emoji for UI display, defaults to `💡` |
| `incrementalCostCpm` | number | no | Cost premium of treatment vs. comparison |
| `measurementSource` | string | no | Source key tying ground-truth records to this hypothesis |
| `measurementLagWeeks` | integer | no | Expected reporting lag, defaults to `1` |
| `minimumTestCells` | integer | no | Minimum cells required for a meaningful test, defaults to `6` |
| `proxyMetrics` | array | no | `{ outcomeType, weight }` proxies that update beliefs alongside the primary metric |
| `groundTruth` | object | no | Free-form expected outcome for sanity checks |
The returned `id` is the `hypothesisId` you'll plug into the test-plan creation step below — and into testability, learning-records, and belief-state queries.
#### Hypothesis status lifecycle
Every hypothesis carries a `status` that the engine advances as evidence accrues. New hypotheses always start at `no_buys`; the rest are reached automatically as media buys link, fire, and produce measurable outcomes.
| Status | Meaning | Triggered when |
| - | - | - |
| `no_buys` | Hypothesis exists but no media is running against it | Hypothesis is created and no allocation entries are linked yet |
| `in_market` | At least one linked buy is delivering | A linked allocation entry / media buy goes active |
| `partially_measured` | Some data has arrived, but coverage is thin | Measurement records start landing but cells / geos are below `minimumTestCells` or coverage gaps remain |
| `well_measured` | Enough data to update beliefs with confidence | Coverage hits `minimumTestCells` across the matched dimensions and the posterior tightens |
| `proven` | Posterior supports the treatment hypothesis | Belief converges with sufficient confidence in the hypothesized direction |
| `disproven` | Posterior rejects the treatment hypothesis | Belief converges with sufficient confidence against the hypothesized direction |
You can filter hypotheses by status when listing —
`GET /advertisers/:advertiserId/hypotheses?status=well_measured` is a useful
default for "what's worth a stakeholder review this week."
### List hypotheses
```bash theme={null}
curl "$BASE/advertisers/12345/hypotheses?take=50&skip=0" \
-H "Authorization: Bearer scope3_"
```
Optional filters: `category`, `status`, `flightId`. Pagination via `take` (max 100) and `skip`.
### Create test cohorts
```bash theme={null}
curl -X POST "$BASE/advertisers/12345/test-cohorts" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"name": "West Coast Treatment",
"description": "Users in CA, OR, WA receiving full ad exposure",
"cohortType": "geographic",
"role": "TREATMENT",
"definition": {
"type": "geo_region",
"regions": ["US-CA", "US-OR", "US-WA"]
},
"estimatedSize": 50000
}'
```
```bash theme={null}
curl -X POST "$BASE/advertisers/12345/test-cohorts" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"name": "Mountain West Control",
"cohortType": "geographic",
"role": "CONTROL",
"definition": {
"type": "geo_region",
"regions": ["US-AZ", "US-NV", "US-NM"]
},
"estimatedSize": 48000
}'
```
| Field | Type | Required | Description |
| - | - | - | - |
| `name` | string | yes | Cohort label, max 255 chars |
| `cohortType` | string | yes | Free-form classification (e.g. `geographic`, `demographic`, `behavioral`) |
| `role` | enum | no | `TREATMENT`, `CONTROL`, or `OBSERVATION` (default `TREATMENT`) |
| `definition.type` | string | yes | Discriminator — `zip_code`, `user_segment`, `geo_region`, `custom` |
| `definition.*` | any | — | Additional fields per type |
| `estimatedSize` | integer | no | Size estimate for power analysis |
Available cohort operations:
| Method | Path | Purpose |
| - | - | - |
| `GET` | `/advertisers/:advertiserId/test-cohorts` | List (filter by `role`, `isActive`) |
| `POST` | `/advertisers/:advertiserId/test-cohorts` | Create |
| `GET` | `/advertisers/:advertiserId/test-cohorts/:id` | Get one |
| `PUT` | `/advertisers/:advertiserId/test-cohorts/:id` | Update |
| `DELETE` | `/advertisers/:advertiserId/test-cohorts/:id` | Archive |
### Create a test plan against a hypothesis
A hypothesis is the question being tested (e.g. "Does running on premium CTV inventory drive incremental purchases versus general CTV?"). Test plans are nested under the hypothesis they test:
```bash theme={null}
curl -X POST "$BASE/advertisers/12345/hypotheses/1f5c0c4e-2c9f-4f0a-9e31-2d4f3c7b1a01/test-plans" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"testCondition": { "inventoryTier": "premium_ctv" },
"controlCondition": { "inventoryTier": "general_ctv" },
"matchDimensions": ["geo", "daypart", "audienceSegment"]
}'
```
| Field | Type | Description |
| - | - | - |
| `testCondition` | object | What defines the treatment arm |
| `controlCondition` | object | What defines the control arm |
| `matchDimensions` | string\[] | Dimensions to balance across cells (at least one) |
### Activate a test plan and link media buys
Once a plan is created (status `designed`), patch it to `active` and link the allocation entries (media buys) that fall under each role:
```bash theme={null}
# Link allocations
curl -X POST "$BASE/advertisers/12345/test-plans//link-buys" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"allocationEntryIds": [
"a1111111-1111-1111-1111-111111111111",
"a2222222-2222-2222-2222-222222222222"
],
"role": "test"
}'
# Activate
curl -X PATCH "$BASE/advertisers/12345/test-plans/" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{ "status": "active" }'
```
`status` transitions: `designed` → `active` → `complete`. Updating to `complete` (or letting the engine auto-complete on flight end) closes the test for inference.
## Step 7: Trigger Learning and Read Belief State
The learning cycle ingests new measurement records, fits the Bayesian update, and refreshes the belief state. It typically runs on a schedule, but you can trigger it on demand after a large data load:
```bash theme={null}
curl -X POST "$BASE/advertisers/12345/learning-cycle/run" \
-H "Authorization: Bearer scope3_"
```
Read the current belief state for an advertiser:
```bash theme={null}
curl "$BASE/advertisers/12345/belief-state" \
-H "Authorization: Bearer scope3_"
```
The response summarizes each tracked hypothesis: posterior magnitude, confidence interval, status (`no_buys`, `in_market`, `partially_measured`, `well_measured`, `proven`, `disproven`), and which sources contributed evidence.
To inspect the records that fed the most recent updates:
```bash theme={null}
curl "$BASE/advertisers/12345/learning-records?\
hypothesisId=1f5c0c4e-2c9f-4f0a-9e31-2d4f3c7b1a01&\
take=50&skip=0" \
-H "Authorization: Bearer scope3_"
```
You can also query raw measurement records by outcome / geo / date range:
```bash theme={null}
curl "$BASE/advertisers/12345/measurement-records?\
outcomeType=incremental_revenue&\
geos=US-CA,US-OR&\
startDate=2026-03-01&endDate=2026-03-31" \
-H "Authorization: Bearer scope3_"
```
Belief state is read-only. To change beliefs, send better data — more
records, fewer gaps, properly designed test plans — and let the next
learning cycle re-fit.
## Step 8: Review and Acknowledge Test Results
When a test plan reaches `complete`, the engine produces an incrementality result attached to the test plan and hypothesis. Read it via the test plan endpoint:
```bash theme={null}
curl "$BASE/advertisers/12345/test-plans/" \
-H "Authorization: Bearer scope3_"
```
The response includes the resolved test cell counts, control cell counts, coverage gaps (geos / dimensions where matching was weak), and the posterior delta on the hypothesis. List all plans for a hypothesis with:
```bash theme={null}
curl "$BASE/advertisers/12345/hypotheses//test-plans?take=50" \
-H "Authorization: Bearer scope3_"
```
### Mark results as reviewed
Acknowledge a result so it stops surfacing in unread-results queues and so the audit trail records who signed off. Use the test plan update endpoint to transition status (e.g. recording observed test/control cell counts and final coverage gaps):
```bash theme={null}
curl -X PATCH "$BASE/advertisers/12345/test-plans/" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"status": "complete",
"testCellsCount": 12,
"controlCellsCount": 11,
"coverageGaps": []
}'
```
If the result motivates a new line of inquiry, capture it as human feedback or as a new hypothesis on the advertiser — that becomes the prior for the next round of testing.
## Best Practices
Events sent to an unknown `event_source_id` are rejected. Always run
`sync_event_sources` first; for new advertisers, do this in your onboarding
automation.
If your MMP / CRM / data warehouse can produce weekly per-geo outcomes,
use `POST /measurement-data/sync` (or `/measurement-records`). Aggregated
records arrive faster, are cheaper to ingest, and are immune to user-level
identity-resolution drift.
For per-user attribution and click-id matching, use the
[Conversion API](/v2/integrations/conversion-api). It's the only path that
can attribute back to a specific impression or click.
Hypothesis `priorConfidence` and `priorMagnitude` are not "the answer" —
they reflect *what you'd believe before the test*. Calibrated priors make
early-flight estimates much more useful than uncalibrated ones.
`matchDimensions` should include any factor that meaningfully drives the
outcome (geo, daypart, segment). Forgetting a strong driver introduces
confounding even with a clean A/B split.
`measurement-data/sync` deduplicates on `external_row_id`. Re-running a
daily export is safe — only changed rows update.
`hashed_email` and `hashed_phone` must be SHA-256 hex (lowercase, 64
chars). Normalize email to lowercase / trimmed and phone to E.164 *before*
hashing. The engine will not match malformed hashes.
`POST /testability` answers "is this design likely to learn anything?"
before media starts. `GET /measurement-freshness` answers "is data
actually arriving?" once it has.
Posterior deltas are a guide, not a verdict. When stakeholders disagree
with a result, capture the disagreement as feedback so the next cycle can
spawn refined hypotheses rather than re-litigating the old one.
## Endpoint Reference
All paths are relative to `https://api.apostra.com/api/v2/buyer`.
### Event sources
| Method | Path |
| - | - |
| `POST` | `/advertisers/:advertiserId/event-sources/sync` |
| `GET` | `/advertisers/:advertiserId/event-sources` |
### Measurement config
| Method | Path |
| - | - |
| `GET` | `/advertisers/:advertiserId/measurement-config` |
| `PUT` | `/advertisers/:advertiserId/measurement-config` |
### Measurement data
| Method | Path |
| - | - |
| `POST` | `/advertisers/:advertiserId/measurement-data/sync` |
| `POST` | `/advertisers/:advertiserId/measurement-records` |
| `GET` | `/advertisers/:advertiserId/measurement-records` |
| `POST` | `/advertisers/:advertiserId/context-records` |
### Events and freshness
| Method | Path |
| - | - |
| `GET` | `/advertisers/:advertiserId/events/summary` |
| `GET` | `/advertisers/:advertiserId/measurement-freshness` |
### Learning engine
| Method | Path |
| - | - |
| `POST` | `/advertisers/:advertiserId/learning-cycle/run` |
| `GET` | `/advertisers/:advertiserId/belief-state` |
| `GET` | `/advertisers/:advertiserId/learning-records` |
| `POST` | `/advertisers/:advertiserId/testability` |
### Measurement sources
| Method | Path |
| - | - |
| `GET` | `/advertisers/:advertiserId/measurement-sources` |
| `POST` | `/advertisers/:advertiserId/measurement-sources` |
| `GET` | `/advertisers/:advertiserId/measurement-sources/:id` |
| `PATCH` | `/advertisers/:advertiserId/measurement-sources/:id` |
### Hypotheses
| Method | Path |
| - | - |
| `GET` | `/advertisers/:advertiserId/hypotheses` |
| `POST` | `/advertisers/:advertiserId/hypotheses` |
| `GET` | `/advertisers/:advertiserId/hypotheses/:id` |
| `PATCH` | `/advertisers/:advertiserId/hypotheses/:id` |
| `DELETE` | `/advertisers/:advertiserId/hypotheses/:id` |
### Test cohorts
| Method | Path |
| - | - |
| `GET` | `/advertisers/:advertiserId/test-cohorts` |
| `POST` | `/advertisers/:advertiserId/test-cohorts` |
| `GET` | `/advertisers/:advertiserId/test-cohorts/:id` |
| `PUT` | `/advertisers/:advertiserId/test-cohorts/:id` |
| `DELETE` | `/advertisers/:advertiserId/test-cohorts/:id` |
### Test plans
| Method | Path |
| - | - |
| `GET` | `/advertisers/:advertiserId/hypotheses/:hypothesisId/test-plans` |
| `POST` | `/advertisers/:advertiserId/hypotheses/:hypothesisId/test-plans` |
| `GET` | `/advertisers/:advertiserId/test-plans/:id` |
| `PATCH` | `/advertisers/:advertiserId/test-plans/:id` |
| `POST` | `/advertisers/:advertiserId/test-plans/:id/link-buys` |
## Related
Send per-user purchase, lead, and engagement events with click and identity
matching.
Set up the campaigns and media buys that measurement data attaches to.
Generate and manage API keys for measurement requests.
Pull aggregated performance once measurement data is flowing.
# Talk to Apostra in Slack, Teams, Discord, or WhatsApp
Source: https://docs.apostra.com/v2/guides/messaging-apps
Authorize the shared Slack app or connect a branded Teams, Discord, or WhatsApp app, then approve its people and locations
Your team can talk to Apostra where they already work: Slack, Microsoft Teams,
Discord, or WhatsApp. Each connection belongs to one Apostra account.
People still receive only the data and actions their linked Apostra
membership allows.
On the seller's Listing page, the Distribution tabs show each messaging
channel's status: Slack always appears, showing **Install in Slack** once
Apostra's Slack app is publicly available — until then it shows
**Coming soon**. Microsoft Teams, Discord, and WhatsApp appear as tabs once
the account is in early access for branded apps; until then they are
offered in the **+ Add destination** picker. Every tab links to the secure
setup pane (**Settings → Messaging apps**), where provider client secrets,
bot tokens, Meta access tokens, and app secrets are entered — never in
email, chat, or a support ticket. Buyer accounts, which have no Listing
page, manage the same apps directly from **Settings → Messaging apps**.
Branded messaging apps are currently available only to accounts enrolled in
the pilot; the secure setup pane shows when the account is not enabled.
Enrollment controls setup and administration only; it does not stop an
already active provider installation.
## Open messaging destinations
The Listing page's Distribution tabs (and, for buyer accounts, **Settings →
Messaging apps** directly) separate the shared app from customer-branded
apps:
* **Slack**'s tab shows **Install in Slack**, which opens Slack's
authorization flow in a new tab, once Apostra's shared Slack app is
publicly available. Slack is currently the only shared, one-click app.
Finish authorization in Slack; Apostra does not show its installation
status yet. Until the shared app is available, the tab shows **Coming
soon**.
* **Microsoft Teams**, **Discord**, and **WhatsApp** each show a **Set up in
Settings** button (**Manage in Settings** once an app already exists) that
opens the secure setup pane. There, connect a Discord, Microsoft Teams, or
WhatsApp app you created with the provider to the current Apostra
account. It then shows the provider webhook endpoint, credential readiness,
approved install locations, and linked people.
Every tab always carries a status chip: **Install available** once the
shared Slack app is public, **N connected** once at least one branded app is
registered for that channel, or **Not set up** before the first one is
registered. Only account admins additionally get the button to act on it —
**Install in Slack**, **Set up in Settings**, or **Manage in Settings**;
everyone else sees just the chip.
Each channel's Distribution tab lists at most 20 branded apps. Once a channel
has more than that, the tab shows the first 20 followed by a note reading
"Showing 20 of *N* branded apps — manage the full list in Settings," where
*N* is the channel's true total; the rest of the roster still exists and is
reachable from **Settings → Messaging apps**.
Shared Discord, Teams, and WhatsApp apps are not available. Customer-branded
Slack apps are also not available. WhatsApp is always reply-only. It does not
initiate conversations or send proactive or template notifications.
## Choose a channel
| Channel | Where people can talk to the agent | How a conversation starts | Product-initiated notifications |
| - | - | - | - |
| Slack | Personal chat and an approved workspace channel | Send a message, or mention Apostra in a channel | Supported only for approved events and destinations captured from the authorized workspace |
| Microsoft Teams | Personal chat and an approved team or channel | Send a personal message, or mention the app in a channel | Supported only for approved event types and locations where the app is installed; rollout is controlled per installation |
| Discord | A user install or an approved server channel | Run `/ask`, or mention the app in a server channel | Supported only for approved event types and authorized install targets; rollout is controlled per installation |
| WhatsApp | A direct conversation with the registered business number | The person sends a text message | Not supported. The app replies only inside Meta's current user-initiated customer-service window |
Slack, Teams, and Discord keep separate conversations for separate install
locations. In Teams, a channel thread also keeps its own context. The initial
Discord experience correlates follow-up `/ask` commands but does not promise a
native Discord thread.
## Start a new conversation
Ordinary follow-up messages in the same approved location keep the linked
person's Apostra context for up to 24 hours after the most recent message. After
24 hours without a message, Apostra automatically starts a new Apostra
conversation before handling the next message. Messages and files from the
expired conversation are not included. In an enabled branded app, use the
reset command when you want to start over sooner:
* In a Discord `/ask`, enter `/reset` as the entire `question`. In an approved
server channel, mention the bot followed only by `/reset`.
* In a Teams personal chat, send `/reset`. In an approved channel thread, reply
in that thread and mention the app followed only by `/reset`.
* In WhatsApp, send `/reset` as the entire message.
Apostra creates a new conversation for that person in that provider
location. The next message does not include messages or files from the previous
conversation.
The reset applies only to the linked Apostra member who sent it and only in
that provider conversation or thread. It does not reset another person's
context, another channel, or another account. It also does not delete the old
provider transcript or Apostra audit history; it changes which Apostra
conversation receives future messages. The shared Slack app follows its
existing channel and thread behavior and does not support this branded-app
`/reset` command.
## What you need before setup
Prepare the following for every branded app:
* A display name, short description, support URL, privacy-policy URL, and
optional terms URL.
* Reviewed square and outline artwork that meets the provider's current app
requirements, plus an accent color where supported.
* Apostra account the app should use with Apostra.
* The exact people and install locations that should be allowed to use it.
A branded setup uses a separate provider registration for the client. It is not
a theme that changes one shared marketplace listing per installation. Provider
naming, publisher, artwork, review, and app-count policies still apply.
## Pilot rollout
Start with one test location and a small group of active Apostra members.
Test Discord `/ask` and a Teams personal chat first, then channel mentions. Add
WhatsApp after Meta approves the business and phone number because WhatsApp is
reply-only.
## Slack setup
1. On the Listing page, open the **Distribution → Slack** tab and select
**Install in Slack**. (A buyer account opens **Settings → Messaging apps**
directly.)
2. In the new tab, choose the Slack workspace and approve the requested access.
3. Finish authorization in Slack. Apostra does not show the installation
result yet, so a completed Slack confirmation is the source of truth.
4. Message Apostra directly, or add it to an approved channel and mention it.
Access to account data still depends on the person's active Apostra
membership and the channel's approved account binding.
This shared Slack app is separate from the incoming webhook used for account
notifications. Authorizing Apostra does not configure the notification webhook or
its event filters.
## Microsoft Teams setup
1. Your Microsoft 365 administrator creates or approves the Microsoft Entra
application and Azure Bot registration for the branded app. The Bot's
messaging endpoint is the HTTPS endpoint supplied during Apostra
provisioning.
2. Apostra generates a Teams app package bound to that exact application ID.
Review its display name, descriptions, publisher links, accent color, and
192 px color and 32 px outline icons.
3. Your administrator uploads the package for a pilot or approves it through
the organization's Teams app policy. Marketplace publication is optional
for a controlled pilot.
4. Install the app in each approved personal, team, or channel location. Team
and channel installation does not by itself grant Apostra access.
5. Link each Microsoft Teams user to an active member of the same Apostra
account. An unlinked person receives a link-account prompt and no account
data.
6. Test a personal message, then mention the app in an approved channel. A
channel message that does not mention the app is ignored.
The runtime validates the Microsoft bearer token, issuer, and exact app
audience before reading a message. The app does not accept an email address or
UPN as a destination for a notification. A proactive message, when enabled,
can go only to a conversation captured from an active installation.
The initial Teams package requests `personal` and `team` bot scopes. Group
chat remains disabled until its separate installation lifecycle is supported.
These scopes identify an install location; they do not replace Apostra
membership or identity linking.
## Discord setup
1. Create or approve the client's Discord application and branded bot in the
Discord Developer Portal.
2. On the Listing page, open the **Distribution → Discord** tab (or the **+
Add destination** picker if the account is not yet in early access) and
select **Set up in Settings**. (A buyer account opens **Settings →
Messaging apps** directly.) Then open **Discord → Add a branded app**.
Enter the application's ID, public key, bot token, display name,
description, and legal links. Copy the interaction endpoint that
Apostra returns into Discord's **Interactions Endpoint URL** field.
Leave **Linked Roles Verification URL** empty.
3. In the Discord Developer Portal, configure the interaction endpoint,
Guild/User Install settings, and exact global `/ask` command on the matching
application.
4. Choose user installation, server installation, or both. For a server
install, approve only the servers and channels where the agent should be
used.
5. Register each approved install location in the secure setup pane, then link
each Discord user to an active member of the same Apostra account.
6. Run `/ask question:...` in an approved direct or server context. The app
acknowledges the command immediately and edits that response when the
agent finishes.
7. In an approved server channel, mention the installed bot and add the
question. Only explicit mentions are accepted; unrelated channel messages
are ignored before account lookup.
The app uses signed HTTP interactions for commands and a singleton Gateway
worker for explicit server mentions. It does not read general server chat or
request Discord's Message Content privileged intent. A bot token is needed for
the Gateway and durable app-initiated delivery and is stored as a secret, never
in the app definition returned to an administrator.
### Existing Apostra installations
An existing Discord installation can be renamed to **Apostra** without asking
members to reinstall it. Apostra keeps the same Discord application ID, bot
token, public key, interaction endpoint, install settings, permissions,
approved locations, linked people, and conversation references. The `/ask`
command stays `/ask`; it has no product name, so there is no second command or
compatibility period to learn.
Your administrator must first confirm whether the Discord app is verified. If
it is verified, Discord restricts application and bot username changes and the
administrator must request the change from Discord Developer Support before
changing anything. If the rename is allowed, the administrator changes the
application name and bot username to **Apostra**, uses the registered purple
Apostra icon, and verifies the result in an existing server and DM before
rolling it out more widely. Contact [Apostra Support](mailto:support@apostra.com)
if your existing install does not show the new name after that check.
## WhatsApp setup
1. In Meta Business Manager, complete the business, display-name, app, WhatsApp
Business Account, and phone-number steps required for production Cloud API
access.
2. Provision the exact Meta app, WhatsApp Business Account, and registered
phone-number IDs with Apostra. Enter the app secret, system-user access
token, and a unique webhook verification token only in the secure flow.
3. Configure the supplied HTTPS callback in the Meta app using the same
verification token and subscribe the app to the WABA `messages` field.
4. Link each WhatsApp user ID to an active member of the same Apostra
account.
5. From that phone, send a current text message to the registered business
number and verify that the reply arrives in the same conversation.
WhatsApp is intentionally reply-only. Apostra does not expose template
messages, scheduled messages, broadcasts, arbitrary recipients, or proactive
notifications on this channel. Status events and non-text messages are
acknowledged but do not invoke the agent. If the user-initiated service window
has closed before the answer is ready, the service does not send it.
## Approvals and secure forms
When an agent proposes a governed change, Teams shows **Approve** and
**Cancel** actions in an Adaptive Card and Discord shows equivalent buttons.
Only the linked person who requested the change can use those controls, in the
same provider conversation and before the confirmation expires. A control is
single-use; removing the person's membership, identity link, installation, or
app access also makes a pending control unavailable.
WhatsApp does not receive interactive approval controls. The agent instead
asks for the exact typed confirmation in the active user-initiated conversation.
This remains a synchronous reply path and does not enable later notifications.
Never paste passwords, API tokens, access keys, or other credentials into any
of these chats. Teams and Discord provide a link action to the scoped secure
credential form when one is needed. WhatsApp returns the same HTTPS handoff as
text. Submit the form outside the provider transcript, then reply `done` in the
conversation to continue.
## Permissions and safety
Provider installation proves where the app is installed; it does not prove
who the sender is inside Apostra. Every sender must be explicitly linked
to a current account member. Removing that membership, identity link, or
installation stops new agent turns at that boundary.
Credentials are encrypted and never returned by the provisioning API. Raw
provider user, tenant, channel, server, and phone identifiers are not used as
Apostra authorization. Provider events are authenticated and deduplicated
before the agent runs.
## Settings and API reference
The `/api/v2/messaging-apps` routes below are the public account REST surface
behind the secure setup pane (**Settings → Messaging apps**, reached from the
Listing page's Distribution tabs for seller accounts). They provision
customer-owned apps, install locations, credentials, and identity links. They
are separate from the provider-facing webhook endpoints returned during
provisioning.
Every account endpoint requires a directly authenticated, active administrator
from a human WorkOS-backed Apostra session and an account enrolled in the
messaging-apps pilot, with one exception: [checking shared Slack
availability](#check-shared-slack-availability) is not pilot-gated. API keys,
service tokens, simulated sessions, staff impersonation, and a linked provider
identity are not administrator credentials for this surface. Requests are
scoped to the administrator's authenticated account; `definitionId` and
`installationId` are UUIDs. An account that is not enrolled receives
`FEATURE_NOT_ENABLED` (HTTP 403), which is an availability result rather than
an empty app list.
Provider and Apostra authentication stay distinct:
| Boundary | Authentication contract |
| - | - |
| Account REST routes | Human WorkOS-backed Apostra session, current active administrator membership, and pilot enrollment. Do not call these routes with an Apostra API key or provider token. |
| Discord interactions and Gateway | Discord HTTP requests must have a valid Ed25519 signature for the definition's required public key. The required bot token is verified against the exact application and powers the Gateway and installed-app delivery. The Gateway requests only `GUILDS` and `GUILD_MESSAGES`; Discord's direct-message and explicit-mention exceptions provide the admitted content without the privileged Message Content intent. |
| Microsoft Teams activities | Microsoft bearer tokens must have the expected issuer and the definition's exact application ID as audience. The required client secret and application-tenant setting belong to that same Azure Bot registration. |
| WhatsApp callbacks and replies | Webhook verification uses the required verify token; event delivery must have a valid `X-Hub-Signature-256` for the required app secret. The required system-user access token is used only for Cloud API replies. WhatsApp remains reply-only inside the current user-initiated service window. |
Provider credentials are supplied only in create or rotation request bodies,
encrypted at rest, and never returned. The webhook paths do not accept an
Apostra administrator session as a substitute for provider authentication.
### Inspect configured apps
`GET /api/v2/messaging-apps/discord`, `GET /api/v2/messaging-apps/teams`, and
`GET /api/v2/messaging-apps/whatsapp` each return `{ definitions: [...] }` for one
provider. The result is sanitized: it never contains provider credentials, raw
provider actor IDs or their digests, message content, or webhook payloads.
| Response field | Meaning |
| - | - |
| `definitions[].definition.id` | App-definition UUID. |
| `definitions[].definition.customerId` | Owning Apostra account ID. |
| `definitions[].definition.agentKey` | Agent configured for the app. |
| `definitions[].definition.brand` | App identity: required `displayName`, `supportUrl`, and `privacyPolicyUrl`; optional `shortDescription`, six-digit `accentColor`, `iconAssetId`, and `termsUrl`. Legal URLs are HTTPS. |
| `definitions[].definition.providerRef` | Provider registration reference. Discord and Teams return `provider` and `applicationId`; WhatsApp returns `provider`, `businessAccountId`, and `phoneNumberId`. |
| `definitions[].definition.status` | `draft`, `active`, or `retired`. |
| `definitions[].credentialConfigured` | Whether the required provider credentials are present; credentials themselves are never returned. |
| `definitions[].installations[]` | Approved provider locations for this definition. |
| `installations[].installation` | Installation UUID, parent `appDefinitionId`, `customerId`, provider-specific `providerRef`, `status` (`active`, `suspended`, or `uninstalled`), and `installedAt`. Discord references `installContext` (`guild` or `user`) and `ownerId`; Teams references `tenantId` and a `personal`, `team`, or `channel` location; WhatsApp references its business-account and phone-number IDs. |
| `installations[].credentialConfigured` | Whether the installation can use the definition's required credentials. |
| `installations[].identityBindings[]` | The linked Apostra person: opaque `bindingId` (used to revoke that exact link), `userId`, `userEmail`, `userName`, `linkedByUserId`, nullable `permissionLevel`, `membershipActive`, `status` (`active` or `revoked`), `verifiedAt`, and nullable `revokedAt`. This intentionally identifies Apostra membership, not the provider actor ID. |
| `installations[].conversationBindings` | Content-free conversation counts: `total`, `direct`, `channel`, and nullable `lastBoundAt`. |
| `installations[].inboundReceipts` | Content-free inbound counts: `received`, `processing`, `completed`, `ignored`, `failed`, and nullable `lastReceivedAt`. |
| `definitions[].interactionEndpoint` | Discord-only HTTPS path to configure in the Discord Developer Portal. |
| `definitions[].messagingEndpoint` | Teams-only HTTPS messaging endpoint. |
| `definitions[].webhookEndpoint` | WhatsApp-only HTTPS callback endpoint. |
### Create an app definition
Use `POST /api/v2/messaging-apps/discord`, `POST /api/v2/messaging-apps/teams`, or
`POST /api/v2/messaging-apps/whatsapp`. Each response returns `definition` (the
sanitized definition above) and the provider-specific endpoint
(`interactionEndpoint`, `messagingEndpoint`, or `webhookEndpoint`). The request
body is strict: do not send fields for another provider.
| Endpoint | Required provider fields |
| - | - |
| `POST /api/v2/messaging-apps/discord` | `applicationId` (15–22 digit Discord ID), 64-character hexadecimal `publicKey`, and `botToken` (32–2,048 characters). |
| `POST /api/v2/messaging-apps/teams` | UUID `applicationId`, UUID `applicationTenantId` or `organizations`, and `clientSecret` (16–4,096 characters). |
| `POST /api/v2/messaging-apps/whatsapp` | Numeric `businessAccountId` and `phoneNumberId` (6–32 digits), `appSecret` (16–4,096 characters), `accessToken` (32–8,192 characters), `webhookVerifyToken` (32–512 characters), and a `graphApiVersion` such as `v24.0`. |
| Every create request | `brand`: required non-empty `displayName` (up to 80 characters), `supportUrl`, and `privacyPolicyUrl`; optional `shortDescription` (up to 200 characters), `accentColor` (`#RRGGBB`), `iconAssetId`, and `termsUrl`. URLs must use HTTPS. |
Provider secrets are accepted only in the request, encrypted, and never
returned by any endpoint. Replacing an app definition does not make a shared
marketplace app; each definition is the customer's own provider registration.
### Register and manage locations
After configuring the endpoint with the provider, register each permitted
location with one of the following strict request bodies. Each successful
registration returns `{ installation }`, using the installation fields in the
inspection table above.
| Endpoint | Request fields |
| - | - |
| `POST /api/v2/messaging-apps/{definitionId}/discord-installations` | `installContext` (`guild` or `user`) and 15–22 digit Discord `ownerId`. |
| `POST /api/v2/messaging-apps/{definitionId}/teams-installations` | UUID `tenantId` and `location`: `{ kind: "personal", userId }`, `{ kind: "team", teamId }`, or `{ kind: "channel", teamId, channelId }`. Each ID is a non-empty provider ID. |
| `POST /api/v2/messaging-apps/{definitionId}/whatsapp-installations` | `{}`. The definition's registered business number is the one permitted location. |
| `PATCH /api/v2/messaging-apps/{definitionId}/installations/{installationId}` | `{ status: "suspended" }` to stop new work while retaining the record, or `{ status: "uninstalled" }` to remove the location. Returns `{ installation }`. |
Suspending or uninstalling a location stops new inbound work and revokes saved
proactive targets. A Teams removal event also suspends its matching installation
automatically; the control does not depend on a provider token continuing to
work after removal.
### Link or revoke a person
`PUT /api/v2/messaging-apps/{definitionId}/installations/{installationId}/identity`
links one provider actor to an active member of the authenticated account. Send
the strict body `{ externalActorId, userId }`, where `externalActorId` is the
trimmed provider user ID (1–255 characters) and `userId` is a positive
Apostra member ID. It returns `{ linked: true, userId }`.
The secure setup pane lists each linked Apostra person, email, membership
state, and link status so an administrator can audit who has access. It
deliberately does not display the raw provider user ID: that identifier is not needed for
routine access review and must not be exposed through a diagnostic response.
Each audited link instead carries an opaque `bindingId`. To remove that exact
mapping, send `DELETE /api/v2/messaging-apps/{definitionId}/installations/{installationId}/identity`
with `{ bindingId }`; the server scopes the handle again to the authenticated
account, definition, installation, and provider before revoking it. Clients
that do not have the audit response may continue to send `{ externalActorId }`
instead. Send exactly one selector. The response is `{ revoked: boolean }`; the
next provider interaction fails closed before the agent runs.
### Rotate credentials or retire an app
The secure setup pane offers both controls directly on each branded app:
**Rotate credentials** opens the same credential fields used at setup (never
prefilled — the pane never displays an existing secret), and **Retire app**
asks for confirmation before it stops the app for good. Both need an account
administrator; the REST calls behind them are:
`PUT /api/v2/messaging-apps/{definitionId}/credentials` replaces credentials and
returns `{ definition, rotated: true }`. Its strict body declares `provider`
and only that provider's credential fields:
| Provider | Credential request fields |
| - | - |
| `discord` | 64-character hexadecimal `publicKey` and `botToken` (32–2,048 characters). |
| `teams` | UUID `applicationTenantId` or `organizations`, plus `clientSecret` (16–4,096 characters). |
| `whatsapp` | `appSecret` (16–4,096 characters), `accessToken` (32–8,192 characters), `webhookVerifyToken` (32–512 characters), and `graphApiVersion` such as `v24.0`. |
To retire the entire app, send `PATCH /api/v2/messaging-apps/{definitionId}` with
`{ status: "retired" }`. It returns `{ definition }`. Retirement prevents new
work for the app; use the location controls when only one provider location
should stop.
### Pause an active app
The pilot enrollment flag makes the secure setup pane and the account API
available; it is not a runtime stop for an app that is already installed. Keep
the account enrolled while an administrator applies a lifecycle control.
* To stop one location, suspend its installation in the secure setup pane, or
send `PATCH /api/v2/messaging-apps/{definitionId}/installations/{installationId}`
with `{ status: "suspended" }`. New inbound work stops at that location and
its saved proactive targets are revoked.
* To stop every location for a branded app, select **Retire app** in the
secure setup pane and confirm, or send
`PATCH /api/v2/messaging-apps/{definitionId}` with `{ status: "retired" }`.
New work for that definition stops and its credentials are queued for cleanup.
After either action, refresh the provider list and confirm the expected
`suspended`, `uninstalled`, or `retired` state. If an account was removed from
the pilot before this action, an administrator must have its enrollment restored
first so that they can apply the lifecycle control. Do not remove provider
credentials as an emergency stop; use the audited lifecycle controls.
### Check shared Slack availability
`GET /api/v2/messaging-apps/slack` returns `{ available: boolean, installUrl:
string | null }` for the shared Apostra Slack app described in [Open
messaging destinations](#open-messaging-destinations). Unlike every other
endpoint on this page, it does not require the account to be enrolled in the
messaging-apps pilot: the shared Slack app is not a pilot feature, so any
authenticated member of the account can read whether it can be installed right
now. `available` is `true`, with `installUrl` set, only once Apostra's
shared Slack app is publicly available; otherwise `installUrl` is `null` and
the secure setup pane shows **Coming soon** instead of a working link.
## Pilot acceptance checklist
* Branding and legal links match the client and have provider approval where
required.
* An account administrator has approved every installation and identity link.
* An unlinked user cannot retrieve account data.
* A linked user can complete at least two turns in the same conversation.
* After 24 hours without a message, the linked user's next message starts a
new Apostra conversation without messages or files from the expired context.
* In a branded Discord, Teams, or WhatsApp app, `/reset` starts a new Apostra
conversation for that linked person. The next turn has no messages or files
from the previous conversation, while another member's context is unchanged.
* Teams channel messages work only when the app is mentioned.
* Discord works through `/ask` and explicit guild-channel mentions without
access to unrelated message content.
* Teams and Discord approvals can be used once only by the linked requester in
the original conversation; another user or location is rejected.
* A credential request opens the scoped secure form and never asks for a secret
in the provider transcript.
* WhatsApp replies only to a current inbound text from the same person and
never initiates a conversation.
* Removing an identity or installation prevents later use.
For a failed pilot, record the provider, branded app name, install location,
approximate UTC time, and the safe error shown to the user. Do not include
tokens, secrets, raw webhook payloads, or personal message content.
## Provider references
* [Microsoft Teams app packages](https://learn.microsoft.com/en-us/microsoftteams/platform/concepts/build-and-test/apps-package)
* [Microsoft Teams proactive messaging](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages)
* [Microsoft Teams card actions](https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-actions)
* [Discord interactions](https://docs.discord.com/developers/interactions/receiving-and-responding)
* [Discord user-installable apps](https://docs.discord.com/developers/tutorials/developing-a-user-installable-app)
* [WhatsApp Business Platform](https://www.postman.com/meta/whatsapp-business-platform/overview/)
# Multi-platform reporting
Source: https://docs.apostra.com/v2/guides/multi-platform-reporting
How Apostra reads delivery when one campaign uses more than one seller or platform.
## One campaign, several sources
A campaign can contain media buys from several sellers or platforms. Apostra
keeps each media buy and package in the buyer's reporting hierarchy, then
combines compatible rows for the view you requested. This lets you read one
campaign while preserving the source that supplied each delivery record.
The same rule applies when a single storefront media buy fans out to several
inventory sources. A source's delivery remains a separate leg until Apostra
can prove the values are comparable.
## How duplicate delivery is avoided
The reporting pipeline keeps the latest report for each reporting day, media
buy, and source. The source is part of that key. A retry from one source
replaces that source's earlier report for the same day; it does not replace a
different source's delivery for the same media buy and day.
Send cumulative-to-date delivery again when you retry a webhook or poll
response. Do not manufacture an idempotency header: source delivery is
deduplicated from the report data and source identity, not from a
`X-Scope3-Webhook-Id` header.
## Currency is never guessed
Apostra can add monetary values only when they share one denomination or have
an applicable conversion for the reporting view. If a storefront delivery read
contains spend in more than one currency, it withholds the buy-level currency
and monetary totals and returns `MIXED_CURRENCY_DELIVERY`. Read the source legs
separately instead of adding EUR and USD together.
The buyer reporting endpoint can use booked or historical foreign-exchange
rates for an advertiser's primary currency. Its cross-currency consolidated
view is an estimate for display, not a billing or settlement amount. See
[Currency in reporting](/v2/guides/reporting-overview#currency).
## Coverage and partial reporting
**Coverage** says which part of the requested scope a report represents. A
report is partial when a seller, platform, package, or reporting day in the
requested scope is missing or cannot be included. Partial does not mean zero
delivery.
In current delivery reads, a source that cannot be read is returned as an
error rather than silently treated as zero. A package that cannot be attributed
to exactly one source is omitted from `by_package` and named in the response
errors. Those safeguards keep an incomplete source response from appearing as
a complete campaign total.
Reliable Reporting will make coverage an explicit part of each obligation and
revision. That capability is still in controlled staging and is not available
on the public API today. See [Reliable Reporting](/v2/guides/reliable-reporting).
## Practical checks
When you reconcile a multi-platform campaign:
1. Read a bounded date range and retain the response currency and warnings.
2. Treat a missing source, withheld spend, or omitted package as incomplete
reporting, not as zero.
3. Normalize currencies before comparing values, and keep the response's
warnings and errors alongside the numbers.
4. Use the buyer reporting hierarchy for campaign rollups and the storefront
delivery read when you need to see a routed media buy's source-level errors.
See [Reporting overview](/v2/guides/reporting-overview) for the buyer hierarchy
and [Signing the webhooks you send us](/v2/storefront/inventory-sources/webhook-signing)
for seller callback authentication.
# Notifications
Source: https://docs.apostra.com/v2/guides/notifications
Configure in-app, email, and Slack notifications for events across your buyer workspace.
## Overview
Notifications surface async events from across your buyer workspace — campaign health, creative approvals, agent registrations, optimization suggestions, and more — through three channels: **in-app** (per-user feed), **email** (personal preferences plus an account address), and **Slack** (account-scoped webhook). Users manage their own in-app and personal email preferences. Admins manage the account email address, its event list, and Slack.
The notification system gives every operator a structured event feed for things they
care about — campaign health, creative approvals, agent registrations, optimization
suggestions, and more. Each event is delivered to one or more channels (`in_app`,
`email`, Slack). Users can opt into supported events; always-on operational alerts
are delivered independently of those personal preferences.
## Open the notification inbox in Murph
Buyer and storefront agents can open the **Notifications** Page in Murph. It
lists a bounded page of events, lets you load older results, and keeps **Read**
and **Acknowledged** as separate states. Both states apply only to the signed-in
recipient: reading or acknowledging a broadcast event does not change another
person's inbox.
The Page also shows the current primary-Slack settings for Product Updates and
sustained source-health alerts. Changing delivery does not mark an inbox event
read or acknowledged, and a successful Slack, email, or webhook attempt does
not count as either action. Release history stays in the separate **Release
notes** Page.
For agent builders, v3 `open_page({"page":"notifications"})` returns a
persona-specific Page contract. Buyer and storefront operation aliases are
explicit; if the launch contract is missing, the Page waits instead of
silently trying another API surface.
All endpoints below are mounted under `https://api.apostra.com/api/v2/` and are
accessible to both buyer and storefront users (role-agnostic shared router).
The notification feed is filtered server-side by your `in_app` opt-ins plus
always-on operational event types. A user with no opt-ins can therefore still
see required storefront health and approval events.
## Event taxonomy
Notification types follow a `resource.action` taxonomy so agents can build workflows
around stable event names. The full enum lives in
`apps/api/src/types/notifications.ts`. A representative slice:
`campaign.created`, `campaign.updated`, `campaign.healthy`,
`campaign.unhealthy`, `campaign.completed`, `campaign.deleted`
`creative.approved`, `creative.rejected`, `creative.changes_requested`,
`creative.sync_started`, `creative.sync_completed`, `creative.sync_failed`
`salesagent.registered`, `salesagent.available`, `signalsagent.registered`,
`outcomesagent.registered`, `salesagent.updated`
`optimization.suggestion_received`, `optimization.suggestion_approved`,
`optimization.suggestion_applied`, `optimization.suggestion_failed`
`measurement.received`, `measurement.stale`, `learning_cycle.completed`,
`hypothesis.proven`, `hypothesis.disproven`
`system.warning`, `system.error`, `syndication.completed`,
`syndication.failed`, `audience.synced`
`inventory_source.unhealthy`, `source.action_required`,
`source.action_resolved`, `salesagent.sync_action_required`,
`storefront_adapter_credential.action_required`,
`storefront.composition_paused`, `storefront.products_not_traffickable`,
`storefront.liveness_degraded`, `storefront.liveness_recovered`,
`media_buy.approval_requested`, `creative.review_requested`,
`ad_server.approval_requested`, `inventory_shortfall.review_requested`,
`storefront.media_buy_buyer_paused` —
operational alerts that bypass opt-in for the in-app feed.
`storefront.media_buy_buyer_paused` tells a storefront with manual
media-buy approval that a buyer paused one of its media buys and the pause
was applied; buyer pauses never wait for approval, so it asks for no
action. Storefronts on automatic approval are not sent it. Storefront
health/readiness, ad-server approval, and inventory-shortfall alerts use
account notification settings. Media-buy and creative-review queue alerts
use the primary or optional escalation audience configured for that approval class;
email is sent only when the routing stage enables email. Slack is the one
exception: even always-on types only post
to Slack once you've added that event type to your Slack configuration's
enabled events — connecting Slack doesn't turn these on by itself.
**Optimization suggestions notify you only when there's a decision to make.**
You get `optimization.suggestion_received` when a suggestion proposes a budget
or bid change on a campaign and media buy that are both still active. A
suggestion that carries no budget or bid change doesn't notify — most often a
media buy with no optimization goal, where the suggestion is pacing information
only, but also any run where the optimizer settles on your current allocation.
Neither does a suggestion whose campaign or media buy is no longer active by the
time it reaches you. In both cases the suggestion is still recorded against the
media buy and closed out, so nothing sits in your queue waiting on a decision
you can't make.
**Your storefront's liveness verdict flipping is a proactive notification.**
When the `liveness.state` on [Get readiness](/v2/storefront/tasks/get-readiness)
flips away from `live` (to `live_attention` or `live_critical`), or a further
worsening from `live_attention` to `live_critical`, you get a
`storefront.liveness_degraded` notification carrying the verdict's own reason
line and fix path — you don't have to notice the dot change color. Recovery
back to `live` sends `storefront.liveness_recovered`. A storefront still in
setup (`state: "setup"`) never alerts — setup is not failure. Repeated sweeps
of the same unchanged state never re-notify.
If the same inventory-source change also opens `source.action_required` or
resolves it with `source.action_resolved`, email recipients receive the
specific source-action email only. The `storefront.liveness_*` event remains
in the in-app notification history, so the storefront-level state change is
still recorded without sending a second email for the same health change.
Each notification carries a `status` (`success`, `error`, `warning`, `info`) and a
`data` object with resource IDs (`campaignId`, `creativeId`, `salesAgentId`, …) plus a
human-readable `message`.
**Pending storefront review now sends a notification.** When a buyer submits a
creative or media buy that's waiting on a storefront operator's decision, it
still appears in the operator's Approvals queue and via Murph — and it also
notifies the configured approval audience. In-app is always on; email and Slack
follow that approval class's routing rule. See [Reviewing buyer transactions](/v2/storefront/approvals/overview)
for how operators review and act on pending items.
Each approval notification opens straight to the review it is about. The
in-app entry, the email's **Review creative** / **Review media buy** button,
and the Slack **Open in Apostra** link all open Approvals focused on that
review or media buy, behind your normal sign-in (`data.actionUrl` carries the
same app-relative path for API consumers). A creative-review notification also
names the creative, the buyer, the creative's format, and its size when the
creative states one (`data.creativeName`, `data.buyerName`,
`data.creativeFormat`, `data.creativeSize`, `data.reviewRef`). Notifications
never carry the creative itself or a preview link: the live preview is created
only when you open the review, because preview links expire after 15 minutes
and die when the creative changes.
## Synchronous creative sync errors
Creative sync failures are not always reported as notifications. When a media-buy create or update targets a library-only seller and `sync_creatives` fails before the media-buy request is sent, the API returns a synchronous error instead of continuing silently.
Handle this as an API failure and retry after correcting the creative or seller issue. The message includes the seller and cause:
```text theme={null}
Creative sync to sales agent {id} failed before media buy update: {reason}
```
## Channels
| Channel | Configuration | Best for |
| - | - | - |
| `in_app` | Per-user opt-ins via `PUT /notification-preferences` | Live operator UI feed |
| `email` | Account-level `notificationEmail` plus event-specific user delivery | Critical alerts, audit trail |
| Slack | Account-level webhook + per-event-type filter | Team channels, war rooms |
`email` and `in_app` preferences on `/notification-preferences` belong to the
signed-in user. Admins manage the separate account email event list on
`/notification-email/opt-ins`, but cannot inspect or change another user's
personal preferences. Slack settings are also account-scoped. Approval work for
media buys and creative review is different: the approval routing rule names
its primary users and any optional escalation users, and its channel settings
decide whether those users also receive email or the customer Slack channel
receives one post. A missing account notification email does not broadcast
those routed approval classes to every active user. Ad-server and
inventory-shortfall approvals still use account notification settings until
dedicated routes are available.
Storefront health and readiness emails are always-on operational alerts. They
go to the account notification address and active account users even when those
users have no saved email opt-in. Changing or clearing a user's opt-ins does not
disable these alerts.
## Per-user preferences
Operators control which events appear in their personal feed or reach their
personal email address. Each user can read and replace only their own list.
```bash theme={null}
curl https://api.apostra.com/api/v2/notification-preferences \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Returns `{ "optIns": [{ "notificationType": "campaign.unhealthy", "channel": "email" }, ...] }`.
`PUT /notification-preferences` replaces the user's entire opt-in list. Send up
to 200 entries.
```bash theme={null}
curl -X PUT https://api.apostra.com/api/v2/notification-preferences \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"optIns": [
{ "notificationType": "campaign.unhealthy", "channel": "email" },
{ "notificationType": "creative.rejected", "channel": "in_app" },
{ "notificationType": "salesagent.registered", "channel": "in_app" }
]
}'
```
## Account-level email opt-ins (admin)
Admins set the recipient address and which event types should generate email.
Changing `notificationEmail` is **sensitive** — the recipient receives every
email-routed event for the account (including alert details that may contain
campaign IDs and account context). Treat the field like any other admin
credential: restrict who can edit it, and audit changes. Mutations to
`notificationEmail` are recorded on the audit log and visible in the buyer
activity feed.
```bash Set the notification email (admin) theme={null}
curl -X PUT https://api.apostra.com/api/v2/notification-email \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "notificationEmail": "alerts@brand.example.com" }'
```
```bash Get the notification email theme={null}
curl https://api.apostra.com/api/v2/notification-email \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```bash Replace account-level opt-ins (admin) theme={null}
curl -X PUT https://api.apostra.com/api/v2/notification-email/opt-ins \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"optIns": [
{ "notificationType": "campaign.unhealthy", "channel": "email" },
{ "notificationType": "creative.rejected", "channel": "email" }
]
}'
```
```bash List account-level opt-ins theme={null}
curl https://api.apostra.com/api/v2/notification-email/opt-ins \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
`PUT /notification-email` and `PUT /notification-email/opt-ins` require the caller to
have the `ADMIN` or `SUPER_ADMIN` role. Pass `notificationEmail: null` to clear the
account-wide recipient; per-user email delivery still follows the rules above.
## Slack configuration (admin)
Slack delivery uses an [Incoming Webhook](https://api.slack.com/messaging/webhooks)
URL. Webhooks are admin-only and stored at the account level.
### Murph channel delivery settings
Product Updates and source outage alerts use your account's primary
Murph Slack channel rather than the incoming-webhook event filter above. An
active account member can ask Murph to show or change these settings; Murph
confirms the write before applying it. You can also use the shared v2 API:
```bash List primary-channel delivery settings theme={null}
curl https://api.apostra.com/api/v2/notification-delivery-preferences \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```bash Turn off Product Updates in the primary channel theme={null}
curl -X PUT https://api.apostra.com/api/v2/notification-delivery-preferences \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "stream": "product_updates", "enabled": false }'
```
```bash Send a Slack alert after a source outage lasts 30 minutes theme={null}
curl -X PUT https://api.apostra.com/api/v2/notification-delivery-preferences \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"stream": "source_health_alerts",
"enabled": true,
"thresholdMinutes": 30
}'
```
Source outage Slack alerts can be sent after 10, 30, or 60 minutes, or turned
off. Each alert also includes controls for future outages. Turning these Slack
alerts off does **not** disable immediate in-app or email health notifications.
Product Updates currently support on/off only. Their delivery schedule is fixed;
the API does not expose a frequency value that the sender cannot honor.
**Email opt-ins and Slack `enabledEventTypes` are independent filters.** The account-level email opt-in list (`/notification-email/opt-ins`) controls which events generate **email**; the Slack `enabledEventTypes` list on `/slack-configuration` is a separate per-channel filter applied to **Slack delivery only**. To get a Slack alert for a given event type, that type must be present in `enabledEventTypes` — the email opt-in list has no effect on Slack. Configure both lists independently to route the same event to the channels you want.
```bash theme={null}
curl -X PUT https://api.apostra.com/api/v2/slack-configuration \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://hooks.slack.com/services/T_REPLACE/B_REPLACE/REPLACE_TOKEN",
"enabled": true,
"enabledEventTypes": [
"campaign.unhealthy",
"creative.rejected",
"system.error"
]
}'
```
The URL must start with `https://hooks.slack.com/services/`. Provide at least one
event type — only events on this list are forwarded to Slack.
A Slack incoming-webhook URL is a **credential**: anyone who has the URL
can post messages to that channel without authenticating. Never paste a
real webhook URL into source files, tickets, screenshots, or shared
docs. Store the URL in a secret manager and rotate it immediately if it
leaks (revoke + reissue from the Slack app configuration).
```bash theme={null}
curl -X POST https://api.apostra.com/api/v2/slack-configuration/test \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Posts a confirmation message to the configured webhook so the team can verify
delivery before relying on it.
```bash theme={null}
curl https://api.apostra.com/api/v2/slack-configuration \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
The response masks the webhook URL by default.
```bash theme={null}
curl -X DELETE https://api.apostra.com/api/v2/slack-configuration \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
## Reading the in-app feed
```bash theme={null}
curl "https://api.apostra.com/api/v2/notifications?unreadOnly=true&limit=25" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
| Query param | Type | Notes |
| - | - | - |
| `brandAgentId` | number | Limit to a single brand agent |
| `campaignId` | string | Limit to a single campaign |
| `creativeId` | string | Limit to a single creative |
| `status` | enum | `success` / `error` / `warning` / `info` |
| `types` | comma list | One or more `NotificationEventType` values |
| `unreadOnly` | `true` / `false` | Hide already-read items |
| `limit` | 1..100 | Default 50 |
| `offset` | int | Number of items to skip (offset-based pagination) |
The response includes the page of notifications, plus `totalCount`, `unreadCount`, and
`hasMore` for pagination.
The list endpoint already filters to event types the calling user opted into on the
`in_app` channel — there is no need to pass `types` from the UI. If the user has not
opted into any in-app types the response is an empty array (with `totalCount: 0`).
## Mark read / acknowledge
A notification has two completion states:
* **read** — surfaced to the user; safe to demote in the UI.
* **acknowledged** — the user has acted on it (e.g. approved a suggestion). The feed
hides acknowledged items by default.
```bash Mark a single item read theme={null}
curl -X POST https://api.apostra.com/api/v2/notifications/$ID/read \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```bash Mark every item read (optionally scoped to a brand agent) theme={null}
curl -X POST https://api.apostra.com/api/v2/notifications/read-all \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "brandAgentId": 12345 }'
```
```bash Acknowledge an item theme={null}
curl -X POST https://api.apostra.com/api/v2/notifications/$ID/acknowledge \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
## Endpoint reference
| Method | Path | Purpose |
| - | - | - |
| `GET` | `/notifications` | List in-app notifications (filtered by user opt-ins) |
| `POST` | `/notifications/read-all` | Mark all visible notifications read |
| `POST` | `/notifications/:id/read` | Mark one notification read |
| `POST` | `/notifications/:id/acknowledge` | Mark one notification acknowledged |
| `GET` | `/storefront/approval-routing` | List approval audiences, channels, and SLA settings |
| `PUT` | `/storefront/approval-routing/:kind` | Replace routing for `MEDIA_BUY` or `CREATIVE_REVIEW` (admin) |
| `GET` | `/notification-preferences` | List the calling user's personal opt-ins |
| `PUT` | `/notification-preferences` | Replace the calling user's personal opt-ins |
| `GET` | `/notification-email` | Read the account's email recipient |
| `PUT` | `/notification-email` | Set the email recipient (admin) |
| `GET` | `/notification-email/opt-ins` | List account-level email/Slack opt-ins |
| `PUT` | `/notification-email/opt-ins` | Replace account-level opt-ins (admin) |
| `GET` | `/slack-configuration` | Read Slack webhook config (admin, URL masked) |
| `PUT` | `/slack-configuration` | Create or update Slack webhook (admin) |
| `POST` | `/slack-configuration/test` | Send a test message to the webhook (admin) |
| `DELETE` | `/slack-configuration` | Remove Slack webhook (admin) |
All endpoints validate the standard buyer auth context (`customerId`, `userId`,
`userRole`). Admin-only endpoints reject non-admin callers with `ACCESS_DENIED`.
## Notification subscriptions
The sections above configure channels, opt-ins, and admin-managed routing
directly for each event type. A newer, more structured layer sits alongside
that: a **notification subscription** is an authored record of what you want
to hear about, when it should fire, where it should go, and who is allowed to
change it.
### What a subscription carries
A subscription names:
* **scope and content**: what it's about and how it renders (an `alert`, or a
`digest` for a recurring summary);
* **a trigger**: a recurring **schedule**, or a typed **condition** that fires
when a metric crosses a line;
* **route references**: which delivery destinations it's aimed at; and
* **provenance and mutability**: who installed it, and exactly which parts of
it you're allowed to change.
A subscription can also carry a delivery policy for quiet hours, dedupe,
cooldown, recovery, and expiry.
No trigger type fires yet: neither a schedule nor a condition has an
evaluator or delivery path behind it today, so creating or patching one in is
rejected. The shape above is the ratified contract a future evaluator reads
from — an [Intelligence Unit (IU)](/v2/buyer/billing/how-iu-billing-works)
allowance-consumption condition is the first one planned. Managing
subscriptions (inspecting provenance, mutability, and routes) is available
today through the [v3 preview agent surface](/v2/setup/v3-preview) for
enrolled accounts; authoring one that actually fires is not available yet.
### Required versus optional notices
Every subscription carries **provenance** (who installed it) and
**mutability** (exactly what you're allowed to change about it):
| Provenance | Installed by | You can change |
| - | - | - |
| `user` | You or a teammate, self-authored | Anything: disable it, change its trigger, change its routes |
| `contract` | A specific term in your Apostra agreement | Only what its mutability allows |
| `plan` | Your accepted IU Rate Card | Only what its mutability allows |
| `system` | The platform itself | Only what its mutability allows |
| Mutability flag | Governs |
| - | - |
| `canDisable` | Turning the subscription off |
| `canEditTrigger` | Changing its schedule or condition |
| `canEditRoutes` | Changing where it's delivered |
Your own subscriptions are fully yours: create them, edit their trigger and
routes, or turn them off, freely, once a trigger type is live. A contract-,
plan-, or system-managed notice is different. It exists because of a
commercial fact you already agreed to, not a personal preference, so it stays
visible and inspectable even when you'd rather not see it.
### Boundaries
A subscription references delivery destinations; it doesn't own them. The
Slack webhook and account notification email you configure above ([Slack
configuration](#slack-configuration-admin), [Account-level email
opt-ins](#account-level-email-opt-ins-admin)) are where those credentials and
addresses live: a subscription only points at them by reference and never
carries a credential itself. And a subscription is for being told something,
not for a machine-readable feed: a scheduled data export belongs to
[Reporting](/v2/guides/reporting-overview), not to a notification.
# Pacing Periods
Source: https://docs.apostra.com/v2/guides/pacing-periods
Define multi-period flight schedules that split DRAFT buys into per-period packages at execute time
## Overview
**Pacing periods** let you express a non-flat campaign flight as a sequence of
dated periods, each with its own spend intensity. Use them when a campaign
needs to spend more during a holiday window, less during a quiet stretch, or
explicit dollar amounts in specific months.
When `execute_campaign` is called, each DRAFT media buy that has no per-buy
schedule of its own is split into one package per active period, with budget
allocated according to the schedule. You do not have to compute or set
per-package budgets yourself.
Express each period as a relative multiplier (`1.0` = normal, `2.5` = 150%
heavier). Apostra distributes the campaign budget proportionally by
`days × weight`.
Set explicit dollar amounts per period. Useful when finance hands you a
monthly spend plan you must hit exactly.
**Pacing splits apply at execute time, for DRAFT buys only.** When you call
`execute_campaign`, every DRAFT media buy without its own per-buy schedule is
split into one package per period. Live buys (ACTIVE or PAUSED) are not
affected when you add or change the campaign pacing plan. You can set the plan
before or after creating buys; it takes effect the next time you execute. The
API rejects a write if all periods have already elapsed.
## Concept
A `pacingPeriods` object has two fields:
| Field | Description |
| - | - |
| `mode` | `"weight"` or `"budget"` |
| `periods[]` | Ordered, non-overlapping list of dated periods |
Each period has:
| Field | Required when | Description |
| - | - | - |
| `label` | Always | Human-readable name (e.g. `"Memorial Day Heavy-Up"`) |
| `start` | Always | Inclusive `YYYY-MM-DD` start date |
| `end` | Always | Inclusive `YYYY-MM-DD` end date |
| `weight` | `mode = "weight"` | Positive number ≤ 10 (relative intensity) |
| `budget` | `mode = "budget"` | Positive dollar amount |
Periods must be **inside the campaign flight dates** and must **not overlap**.
Gaps between periods are allowed and are treated as **pauses**: the campaign
spends \$0 during a gap.
## How to use
### Weight mode
Weight mode is the most common: declare the shape, let Apostra do the math.
```bash theme={null}
curl -X POST 'https://api.apostra.com/api/v2/buyer/campaigns' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"advertiserId": "12345",
"name": "Summer 2026",
"flightDates": { "startDate": "2026-06-01T00:00:00Z", "endDate": "2026-08-31T23:59:59Z" },
"budget": { "total": 300000, "currency": "USD" },
"pacingPeriods": {
"mode": "weight",
"periods": [
{ "label": "June ramp", "start": "2026-06-01", "end": "2026-06-30", "weight": 1.0 },
{ "label": "July 4th heavy", "start": "2026-07-01", "end": "2026-07-15", "weight": 2.5 },
{ "label": "Mid-summer cool", "start": "2026-07-16", "end": "2026-08-15", "weight": 0.5 },
{ "label": "Back to school", "start": "2026-08-16", "end": "2026-08-31", "weight": 1.5 }
]
}
}'
```
When the campaign executes, each product is split four ways with budget
proportional to `days × weight`.
### Budget mode
Budget mode pins explicit dollars to each period. The sum of period budgets
must not exceed `budget.total` on the campaign; anything left over stays as
unallocated headroom.
```bash theme={null}
curl -X POST 'https://api.apostra.com/api/v2/buyer/campaigns' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"advertiserId": "12345",
"name": "Q3 Plan",
"flightDates": { "startDate": "2026-07-01T00:00:00Z", "endDate": "2026-09-30T23:59:59Z" },
"budget": { "total": 150000, "currency": "USD" },
"pacingPeriods": {
"mode": "budget",
"periods": [
{ "label": "July", "start": "2026-07-01", "end": "2026-07-31", "budget": 60000 },
{ "label": "August", "start": "2026-08-01", "end": "2026-08-31", "budget": 60000 },
{ "label": "September", "start": "2026-09-01", "end": "2026-09-30", "budget": 30000 }
]
}
}'
```
### Updating a schedule
`PUT /api/v2/buyer/campaigns/:id` accepts the full `pacingPeriods` object. To
clear an existing schedule, send `"pacingPeriods": null`.
```bash theme={null}
curl -X PUT 'https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"pacingPeriods": {
"mode": "weight",
"periods": [
{ "label": "June ramp", "start": "2026-06-01", "end": "2026-06-30", "weight": 1.0 },
{ "label": "July 4th heavy", "start": "2026-07-01", "end": "2026-07-15", "weight": 3.0 },
{ "label": "Mid-summer cool","start": "2026-07-16", "end": "2026-08-15", "weight": 0.5 },
{ "label": "Back to school", "start": "2026-08-16", "end": "2026-08-31", "weight": 1.5 }
]
}
}'
```
Periods that have already started or ended **cannot be modified or removed**.
Updates may only change future periods. Changing the `mode` is rejected if
any existing period is already in progress or past.
## Live cascade for appended periods
When you append new periods to a campaign that already has paced live media
buys, Apostra automatically pushes the new period packages to those media
buys via ADCP 3.0 `update_media_buy.new_packages`. The result is returned on
the update response as `pacingCascadeResult`.
### What gets cascaded
Only **strict appends** are cascaded: the new `pacingPeriods.periods` array
must start with the same items as the existing schedule. Insertions in the
middle, modifications of an existing period, removals, and mode changes
return `pacingCascadeResult.attempted: false` with a `notAppendedReason` and
require manually creating new media buys to apply.
Only media buys that were **already paced** (executed with `pacingPeriods`
set on the campaign at execution time) are eligible. Unpaced media buys are
reported with `outcome: "skipped"`, `reason: "unpaced"`. To apply pacing,
create a new media buy.
### Agent capability
Each seller agent must advertise `add_packages` in its `valid_actions` to
accept new packages mid-flight. Agents that don't are reported with
`outcome: "unsupported"`. Manual workaround: create new media buys for the
appended periods on those agents.
### Response shape
```json theme={null}
{
"campaign": { "...": "..." },
"pacingCascadeResult": {
"attempted": true,
"totalMediaBuys": 2,
"updatedCount": 1,
"skippedCount": 0,
"failedCount": 0,
"unsupportedCount": 1,
"results": [
{
"mediaBuyId": "mb_abc",
"agentName": "Vox",
"outcome": "updated",
"addedPackageCount": 3
},
{
"mediaBuyId": "mb_xyz",
"agentName": "Acme",
"outcome": "unsupported",
"reason": "agent does not advertise add_packages in valid_actions"
}
]
}
}
```
The block is omitted when `pacingPeriods` is not part of the update payload.
## Per-media-buy pacing
Campaign-level `pacingPeriods` shapes the whole campaign: every paced media
buy under it follows the same schedule. When you only want to shape *one*
media buy (heavy-up on a specific publisher with seasonal or event-driven
inventory, leave the rest flat), set `pacingPeriods` on the `mediaBuys[]`
entry of `update_campaign` instead.
The per-buy schedule **replaces** the campaign-level shape for that specific
buy. It uses the same `mode` + `periods[]` shape as the campaign-level
field.
```bash theme={null}
curl -X PUT 'https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"mediaBuys": [
{
"mediaBuyId": "mb_vox_concerts",
"pacingPeriods": {
"mode": "weight",
"periods": [
{ "label": "Pre-tour", "start": "2026-06-01", "end": "2026-06-30", "weight": 1.0 },
{ "label": "Tour heavy-up", "start": "2026-07-01", "end": "2026-07-15", "weight": 3.0 },
{ "label": "Wind-down", "start": "2026-07-16", "end": "2026-08-31", "weight": 0.5 }
]
},
"updated_reason": "Heavy-up on Vox concert inventory during the tour window"
}
]
}'
```
### Behavior by media buy state
| Media buy status | Behavior |
| - | - |
| `DRAFT` / `PENDING_APPROVAL` | Schedule persisted on the buy. At execute time, packages are split by the per-buy schedule instead of the campaign-level schedule. |
| `ACTIVE` / `PAUSED`, already paced, strict append | Appended period packages are sent to the seller via `update_media_buy.new_packages`. Requires the seller to advertise `add_packages` in `valid_actions`. |
| `ACTIVE` / `PAUSED`, unpaced (live flat packages) | **Rejected.** Introducing period packages on top of live flat packages would double-spend. Create a new media buy with pacing periods instead. |
| `ACTIVE` / `PAUSED`, paced, non-strict-append change | **Rejected.** Insertions, modifications, removals, and mode changes are not supported. Create a new media buy. |
Pass `"pacingPeriods": null` to clear an existing per-buy schedule. The
column is cleared but any deployed period packages keep running. To stop
spend on a deployed window, cancel those packages directly.
Periods must lie inside the **media buy's** flight (not the campaign's),
must not overlap, and follow the same future-only update rules as the
campaign-level field. In response shapes, each per-buy period includes a
resolved `budget` computed from the sum of the buy's product budgets.
When per-buy pacing is set on the same `update_campaign` call as
`mediaBuys[].creative_ids` (or relies on auto-sync from the campaign
manifest), the appended period packages inherit those creatives in the
same seller request. There is no separate creative-attach step.
### Budget reductions
Lowering campaign `budget.total` through `update_campaign` can also reduce
live package budgets. If the new media budget is below current live package
allocations, Apostra proportionally reduces eligible live package budgets,
including period packages created from pacing periods. Explicit
`mediaBuys[].packages[].budget` values in the same request take precedence
over proportional scaling.
If Apostra cannot apply a generated seller budget update, the campaign budget is
not lowered. This keeps campaign budget and live package allocations in sync.
## Response shape
Campaign `GET` responses surface the schedule with **resolved budgets** for
every period. In weight mode, the resolved dollar amount is computed by
`(days × weight)` proportional split of `budget.total`; weights are preserved
alongside the resolved budget so you can audit the math.
```json theme={null}
{
"campaign": {
"id": "cmp_987654321",
"budget": { "total": 300000, "currency": "USD" },
"allocatedBudget": 240000,
"unallocatedBudget": 60000,
"pacingPeriods": {
"mode": "weight",
"periods": [
{ "label": "June ramp", "start": "2026-06-01", "end": "2026-06-30", "weight": 1.0, "budget": 60000 },
{ "label": "July 4th heavy", "start": "2026-07-01", "end": "2026-07-15", "weight": 2.5, "budget": 75000 },
{ "label": "Mid-summer cool", "start": "2026-07-16", "end": "2026-08-15", "weight": 0.5, "budget": 31000 },
{ "label": "Back to school", "start": "2026-08-16", "end": "2026-08-31", "weight": 1.5, "budget": 74000 }
]
}
}
}
```
`unallocatedBudget` is the dollar gap between `budget.total` and
`allocatedBudget` (the sum of the budgets of every media buy that still holds
budget — including `DRAFT` buys — plus delivered spend on ended or archived
media buys, all gross and fee-inclusive), telling you how much room is left to
allocate to fill the schedule. It can go negative when delivered spend on ended
buys exceeds `budget.total`.
## Automatic package splitting
When a campaign with pacing periods executes:
Each selected product gets a budget, either set by the user or derived
from a discovery proposal.
For every product, Apostra creates one package per pacing period. In weight
mode, the per-period budget is
`(days × weight) / sum(all weighted days) × product_budget`. In budget mode,
each period's fixed budget is split across products proportionally to
each product's share of total product budgets.
Each period after the first uses banker's rounding to two decimals; the
last period absorbs any rounding remainder so per-product totals match
exactly.
The split packages are sent to the sales agent as the media buy
composition. Each package carries its own `start`, `end`, and budget.
This means a campaign with 10 products and 4 periods produces **40 packages**
on execution, each one a discrete dated bucket of spend.
## Validation
The campaign endpoints validate `pacingPeriods` against the campaign before
saving:
* **Date order**: every period's `start` must be ≤ `end`.
* **Inside flight**: every period must lie within `flightDates.startDate` and
`flightDates.endDate`.
* **No overlaps**: when sorted by `start`, no period may begin on or before
the previous period's `end`.
* **Budget sum** (budget mode): the sum of period budgets must not exceed
`budget.total`.
* **Required fields per mode**: weight periods need a `weight`; budget periods
need a `budget`.
* **Update safety**: a period that has already started cannot be changed or
removed; the mode cannot change while any period is in progress or past.
A failing validation returns a 400 with a clear error message identifying the
offending period by label.
## Best practices
* **Start in weight mode.** It's easier to reason about "2× during the holiday
week" than to recompute exact dollar splits when the budget moves.
* **Use labels generously.** Period labels show up in package metadata and in
reporting; names like `"July 4th heavy"` beat `"Period 2"`.
* **Keep periods aligned with media buy intent.** A media buy can have any
start/end inside the campaign flight, but matching its dates to a period
boundary keeps reporting clean.
* **Treat gaps as pauses.** If you don't want spend on July 16–18, leave that
range out of `periods` rather than setting `weight: 0` (which is rejected
as non-positive).
* **Plan ahead: past periods are immutable.** Once a period starts, its
shape is locked. Front-load decisions you might need to revisit.
* **Cap period count.** The schema allows up to 52 periods; in practice,
monthly or weekly granularity is plenty.
## Limits
| Limit | Value |
| - | - |
| Periods per campaign | 1–52 |
| `weight` range | `(0, 10]` |
| `budget` per period | Positive dollar amount |
| `budget` sum (budget mode) | ≤ `budget.total` |
| Max label length | 100 characters |
| Date format | `YYYY-MM-DD` (UTC, inclusive on both ends) |
# Property Lists
Source: https://docs.apostra.com/v2/guides/property-lists
Curate include and exclude inventory lists across web, mobile, and CTV for your advertisers and propagate them across active media buys
## Overview
A **property list** is a named, advertiser-scoped collection of typed
identifiers (websites, mobile apps, CTV apps) used to shape where a brand's
media runs. Lists carry a `purpose` of either `include` (only buy on these
properties) or `exclude` (never buy on these properties). Once created, lists
are linked to the brand's targeting profile and enforced when you buy:
include lists can travel to supporting sales agents during discovery or as
buy-time targeting. Apostra also filters conflicting inventory at selection
time.
Property lists are a **buyer-curated** concept. They are owned by your
advertiser and validated against the AAO property registry. Supporting sales
agents receive list references that they resolve on demand. Apostra enforces
exclusions on the buyer's side as a backstop.
Restrict targeting to a hand-picked allow-list (e.g., a curated PMP of
premium publishers).
Block specific domains across all of an advertiser's campaigns (e.g., a
brand-safety blocklist).
## How lists are enforced
Enforcement happens at **selection time**: when products are discovered and
when a campaign is executed. Delivery-grain enforcement at the seller
(dropping individual impressions inside an already-running buy) is not yet
available.
* **Include lists** are sent as `filters.property_list` during discovery when
a sales agent declares either
`media_buy.features.property_list_filtering: true` or
`media_buy.execution.targeting.property_list`. The feature declaration
authorizes discovery filtering only. A `targeting_overlay.property_list`
reference is sent on media-buy packages only when the seller declares the
execution-targeting dimension. Sellers without the relevant declaration do
not receive the reference.
* **Exclude lists** are not sent in `get_products`: AdCP discovery filters do
not define `property_list_exclude`. They remain enforced on the buyer side
during discovery and are sent as `targeting_overlay.property_list_exclude`
on media-buy packages only when a sales agent declares
`media_buy.execution.targeting.property_list_exclude` support.
When an advertiser links multiple exclusion catalogs, the seller receives
one reference that resolves to their combined, deduplicated identifiers.
Apostra also enforces the list on the buyer's side: products whose stored
property identifiers match the advertiser's exclusion list are filtered out
of product discovery results, and execution rejects conflicting selections.
Discovery reports the number removed in
`excludedProductCount`, per-storefront counts in
`excludedProductCountsByStorefront`, and bounded product details in
`excludedProductsByStorefront`; each detail contains the product ID, name,
and matching domains plus typed identifiers. Canonical `get_products` reports the same total as
`ext.interchange.excluded_product_count` and each seller row carries
`excluded_product_count` plus bounded `excluded_products`. The domains are
the intersection of that seller product and the buyer's own list, not the
buyer's complete list. When an exclusion empties a completed result, the
response guidance says how many products from how many sellers were removed
because they included excluded properties instead of calling the result a
lack of matching inventory. A legacy candidate without seller identity is
included in the total but not assigned to a seller row, so the guidance
reports the removed-product count without inventing a seller count.
Executing a campaign fails with a clear error naming the conflicting products when a selected product's
publisher is on the list. Remove those products from the buy, or remove the
exclusion, to proceed. Matching uses the typed identifiers the current
property-membership model stores: `domain`, `subdomain`, `ios_bundle`,
`android_package`, `apple_tv_bundle`, `bundle_id`, `apple_app_store_id`,
`google_play_id`, `roku_store_id`, `roku_channel_id`, `fire_tv_asin`,
`fire_tv_app_id`, `amazon_app_store_id`, `samsung_app_id`, `lg_channel_id`,
and `vizio_app_id`. Identifier values are exact after normalisation: domains,
iOS bundles, and Apple TV bundles are lowercased; Android packages, generic
bundles, store IDs, and channel IDs keep their case. Products can declare
every AdCP identifier type, but DOOH venue and podcast list entries are not
stored by this property-list model yet, so they do not match an exclusion
list at selection time.
* **Products sold through an Apostra storefront** take the declaration of the
sales agent behind the storefront, not the storefront's own. The storefront
passes both references to that sales agent unchanged. Apostra reads the
agent's declaration from the AdCP capabilities it last refreshed for it, so a
seller declares `property_list` or `property_list_exclude` in its own sales
agent's capabilities. Managed sales agents and platform adapters publish no
such declaration, so they don't receive references.
* **A list reference you set on a package yourself** (`property_list`,
`property_list_exclude`, `collection_list` or `collection_list_exclude` in a
package's `targetingOverlay`) is sent only to a seller that declares that
exact field under `media_buy.execution.targeting`. A collection-list
reference also needs the product to allow collection targeting
(`collection_targeting_allowed: true`); AdCP treats a product without it as
a bundle. If the seller does not declare the field, executing the media buy,
or updating a media buy the seller already holds, fails with a validation
error that names the package, the field and the seller, and nothing is
sent. A seller that does not declare a field is not
obliged to apply the list, so sending it anyway could let the buy run
without it. Remove the reference from the package, or choose products whose
seller declares the field. If Apostra cannot read the seller's declaration,
the request fails with a retryable error instead.
## Concept
Each list stores a set of typed identifiers that are normalized, deduped, and
resolved through two systems:
1. **AAO registry** — the cross-publisher Ad Context registry used to confirm a
property is a real, identified entry.
2. **Local property catalog** — Apostra's mapping of identifiers to targetable
`Property` records used by sales agents.
Submitted identifiers land in one of three buckets, surfaced in every
create/update response as a `resolutionSummary`:
| Bucket | Meaning | Targets? |
| - | - | - |
| `resolvedCount` | Mapped to a local `Property` record | Yes |
| `registeredCount` | Known to AAO but no local property yet | Not yet — will become targetable as catalog catches up |
| `unresolvedCount` | Not found anywhere | No — silently skipped |
A single list can hold up to **100,000 identifiers** per request. The service
chunks large inputs server-side against the AAO registry (which itself caps at
10,000 domains per call) and returns a single resource with the full
resolution summary.
## Identifier types
Property lists accept AdCP-aligned typed identifiers. Pass a typed array via
`identifiers: [{type, value}]`, or use the `domains: string[]` shorthand
when every entry is a website domain.
| Type | Resolves via | Example value |
| - | - | - |
| `domain` | `Domain.domain` (`SITE`) | `example-times.com` |
| `subdomain` | `Domain.domain` (`SITE`) | `news.example.com` |
| `ios_bundle` | `App.bundle` (Apple App Store) | `com.facebook.katana` |
| `android_package` | `App.bundle` (Google Play) | `com.facebook.katana` |
| `apple_tv_bundle` | `App.bundle` (Apple App Store) | `com.netflix.Netflix` |
| `bundle_id` (generic fallback) | `App.bundle` (any store) | `com.example.app` |
| `apple_app_store_id` | `Domain.domain` (`APPLE_APP_STORE`) | `284882215` |
| `google_play_id` | `Domain.domain` (`GOOGLE_PLAY_STORE`) | `com.example.app` |
| `roku_store_id` | `Domain.domain` (`ROKU`) | `12` |
| `fire_tv_asin` | `Domain.domain` (`AMAZON`) | `B00X4WHP5E` |
| `samsung_app_id` | `Domain.domain` (`SAMSUNG`) | `G19173000091` |
A single mobile app can appear in a property list under multiple identifier
types (e.g. `ios_bundle` AND `apple_app_store_id`); each resolves independently
against the AAO registry / local catalog.
`domains: ["example-times.com"]` is exactly equivalent to
`identifiers: [{ "type": "domain", "value": "example-times.com" }]`. You can pass
both fields in the same request — they're concatenated and deduplicated.
**Read-side normalization.** `apple_tv_bundle` and `ios_bundle` share the
same backing column (`App.bundle` with `appStore=APPLE_APP_STORE`), so an
`apple_tv_bundle` write reads back as `ios_bundle` on subsequent `GET`/`list`
responses. The generic `bundle_id` fallback likewise normalizes to the
resolved app row's store-typed form (`ios_bundle` or `android_package`).
Submit the type that best matches the AdCP property registry; expect the
response to carry the canonical store-typed form.
## Use with a V3 MCP agent
Buyer agents use one V3 write call for a property list's lifecycle:
`save_property_list`. Create a list with its advertiser, name, purpose, and
identifiers; update its name or identifier set with its returned ID; or archive
it with `isArchived: true`. Archiving is currently one-way.
To validate identifiers without saving a list, use the same call with
`check: true`, then pass its returned report ID to
`get(kind: "property_list", include: ["report"], reportId: "...")`; this
does not require a saved-list ID. `get` returns one bounded identifier page at
a time. Use its `identifierPage.nextOffset` with `identifierOffset` to continue,
and set `identifierCategory` to `unresolved` or `registered` when you need a
resolution bucket. Each page item is an AdCP identifier (`type`, `value`). A
value longer than 512 bytes is cut and flagged with `value_truncated: true`; a
cut value no longer matches a property, so do not copy it into another request.
`search` returns compact list summaries rather than identifier arrays. Use
`search(kind: "property_list", filter: { advertiserId: "123" })` to find an
advertiser's lists and `get` for a single list. The advertiser ID is required
because property lists are advertiser-scoped.
Every list belongs to the advertiser it was created for. By default a new
list becomes the advertiser's all-campaigns list for its purpose: it replaces
the previous one (the response names it in `replacedPropertyListIds`; the
replaced list stays saved but applies to no campaign), applies to every
campaign, and is pushed to active media buys. Create it with `appliesToAllCampaigns: false` to save it for
the advertiser without applying it to any campaign; the advertiser's lists and active
media buys are left unchanged. The choice is fixed at creation, and `get` and
`search` return it as `appliesToAllCampaigns`. If the advertiser's brand has no list
slot for the list's channels, a default create cannot apply the list anywhere:
it is saved for campaign use only and returned with
`appliesToAllCampaigns: false`.
## Campaign lists
Every campaign inherits the advertiser's lists. A campaign can also add one
include list and one exclude list of its own, stored on the campaign:
* `campaignPropertyListId` sets the campaign's own include list.
* `campaignPropertyListExcludeId` sets the campaign's own exclude list.
Both fields work the same way on `save_campaign` and on the v2 API's
`POST /campaigns/{campaignId}/property-lists`.
`propertyListId` is deprecated on both; use `campaignPropertyListId`. Until
2026-12-31 a non-null `propertyListId` keeps its old behaviour: it applies the
named include list, which must already be configured for the advertiser, to
the campaign's active media buys, combined with the campaign's own include
list, without storing it as the campaign's own list, and the response carries a `deprecationNotice` naming the replacement.
`propertyListId: null` is refused with a validation error, on `save_campaign`
and on the v2 endpoint alike. It used to remove property-list targeting from
the campaign's live buys, which would leave them running without the
advertiser's lists. To clear the campaign's own list, send
`campaignPropertyListId: null`; the advertiser's lists keep applying. After
2026-12-31 `propertyListId` is refused.
Each must be a list of the right purpose that belongs to the campaign's
advertiser. A list saved with `appliesToAllCampaigns: false` is the usual
choice, because it applies only where a campaign uses it. Set either field to
`null` to clear the campaign's own list; that never removes an advertiser list.
Both fields need an existing campaign (`campaignId`).
The lists combine the way you would expect:
* **Exclusions add up.** A property is excluded if the advertiser's exclude
lists or the campaign's exclude list contain it. A campaign cannot remove an
advertiser exclusion.
* **Inclusions narrow.** When both the advertiser and the campaign have an
include list, a property must be on both.
A media-buy package carries one reference per field, so when more than one list
applies Apostra hosts one combined list and sends that reference. Sellers
resolve it through the same `GET /lists/:listId` endpoint as any other list.
Saving a campaign's lists pushes the lists in effect to the campaign's active
media buys, and every buy created afterwards carries them from its first
dispatch. Archiving a list clears it from any campaign that uses it.
Read the campaign with `get(kind: "campaign", include: ["propertyLists"])` (or
`GET /api/v2/buyer/campaigns/:campaignId/property-lists`). The response has:
| Field | Meaning |
| - | - |
| `campaignLists` | The campaign's own `campaignPropertyListId` and `campaignPropertyListExcludeId` (the same names `save_campaign` takes, so a read can be written back unchanged), each a list ID or `null` |
| `effective.include` / `effective.exclude` | The reference sent to sellers (`listId`) and the lists it combines (`listIds`, advertiser lists first), or `null` when none applies |
| `propertyLists` | The lists the campaign's media-buy packages currently reference, with the buys and packages that carry each one |
The same combination applies when a campaign is executed: products whose
properties are on the advertiser's or the campaign's exclude lists are
rejected. Product discovery filters by the advertiser's lists only; a
campaign's own lists take effect when its media buys are executed.
## Endpoints
All buyer endpoints below are mounted under `https://api.apostra.com/api/v2/buyer`.
The `GET /lists/:listId` resolution endpoint is mounted at the **app root**
(no `/api/v2/buyer` or `/api/v2/storefront` prefix) per ADCP convention.
| Method | Path | Purpose |
| - | - | - |
| `GET` | `/api/v2/buyer/advertisers/:advertiserId/property-lists` | List lists for an advertiser |
| `POST` | `/api/v2/buyer/advertisers/:advertiserId/property-lists` | Create a list |
| `GET` | `/api/v2/buyer/advertisers/:advertiserId/property-lists/:listId` | Get one list |
| `PUT` | `/api/v2/buyer/advertisers/:advertiserId/property-lists/:listId` | Replace a list's domains and/or name |
| `DELETE` | `/api/v2/buyer/advertisers/:advertiserId/property-lists/:listId` | Archive a list |
| `POST` | `/api/v2/buyer/property-lists/check` | Validate a candidate domain set without creating a list |
| `GET` | `/lists/:listId` | High-cardinality public read used by sales agents (HMAC token auth, mounted at app root) |
All `propertyList`-returning responses use the `{ propertyList }` wrapper.
List endpoints return `{ propertyLists, total }`.
## How to use
### Create an include list
Domains-only shorthand:
```bash theme={null}
curl -X POST 'https://api.apostra.com/api/v2/buyer/advertisers/12345/property-lists' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"name": "Q1 UK Premium",
"purpose": "include",
"domains": ["example-times.com", "example-press.co.uk", "example-news.com"],
"filters": {
"channels_any": ["display", "olv"],
"countries_all": ["GB"]
}
}'
```
Mixed web + mobile + CTV:
```bash theme={null}
curl -X POST 'https://api.apostra.com/api/v2/buyer/advertisers/12345/property-lists' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"name": "Cross-screen premium",
"purpose": "include",
"identifiers": [
{ "type": "domain", "value": "example-times.com" },
{ "type": "ios_bundle", "value": "com.exampletimes.ExampleTimes" },
{ "type": "android_package", "value": "com.exampletimes.android" },
{ "type": "apple_app_store_id", "value": "284862083" },
{ "type": "roku_store_id", "value": "12" }
]
}'
```
Response:
```json theme={null}
{
"propertyList": {
"listId": "42",
"name": "Cross-screen premium",
"purpose": "include",
"identifiers": [
{ "type": "domain", "value": "example-times.com" },
{ "type": "ios_bundle", "value": "com.exampletimes.ExampleTimes" },
{ "type": "android_package", "value": "com.exampletimes.android" }
],
"unresolvedIdentifiers": [
{ "type": "apple_app_store_id", "value": "284862083" },
{ "type": "roku_store_id", "value": "12" }
],
"registeredIdentifiers": [],
"domains": ["example-times.com"],
"unresolvedDomains": [],
"registeredDomains": [],
"propertyCount": 14,
"resolutionSummary": {
"totalRequested": 5,
"resolvedCount": 3,
"registeredCount": 0,
"unresolvedCount": 2,
"resolutionRate": 0.6
},
"createdAt": "2026-04-25T10:30:00.000Z",
"updatedAt": "2026-04-25T10:30:00.000Z"
}
}
```
**Response fields**
* `identifiers` — typed `{type, value}[]` actually resolved to local Property
rows. **Persisted** as catalog membership.
* `unresolvedIdentifiers` — submitted identifiers with no matching local
Property record. **Transient** — see persistence note below.
* `registeredIdentifiers` — identifiers found in the AAO registry but not yet
locally targetable. Today only `domain` entries can land here. **Transient**.
* `domains` / `unresolvedDomains` / `registeredDomains` — convenience views of
the above filtered to `type: "domain"`. App identifiers are **not** in these.
* `resolutionSummary` — counts and `resolutionRate` (0..1) over the
deduplicated, normalized input.
`unresolvedIdentifiers` and `registeredIdentifiers` are **transient output of
the resolution call**, not persisted state. They appear on create/update
responses, then drop on subsequent `GET`s and on name-only `PUT`s. Only
`identifiers` (the resolved set) is persisted on the list. Re-submit the
identifier set on a `PUT` to re-surface them.
### Update a list
A `PUT` replaces the full identifier set. Active media buys that reference
this list are then notified — see [Cascade behavior](#cascade-behavior).
You can pass `domains`, `identifiers`, or both:
```bash theme={null}
curl -X PUT 'https://api.apostra.com/api/v2/buyer/advertisers/12345/property-lists/42' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"identifiers": [
{ "type": "domain", "value": "example-times.com" },
{ "type": "ios_bundle", "value": "com.exampletimes.ExampleTimes" }
]
}'
```
The update response includes `resolutionSummary` and `cascadeSummary` whenever
identifiers change. A name-only `PUT` returns neither.
### Validate before you commit
Use `POST /api/v2/buyer/property-lists/check` to lint a candidate identifier set
against curation rules without creating anything:
```bash theme={null}
curl -X POST 'https://api.apostra.com/api/v2/buyer/property-lists/check' \
-H 'Authorization: Bearer scope3_' \
-H 'Content-Type: application/json' \
-d '{
"identifiers": [
{ "type": "domain", "value": "example-times.com" },
{ "type": "domain", "value": "duplicate.com" },
{ "type": "domain", "value": "duplicate.com" },
{ "type": "ios_bundle", "value": "com.facebook.katana" }
]
}'
```
The response groups entries into action buckets:
| Bucket | Meaning |
| - | - |
| `ok` | Clean — safe to include |
| `modify` | Canonicalized (e.g., `WWW.example.com` → `example.com`) |
| `remove` | Drop — duplicate or blocked |
| `assess` | Manual review recommended. **All non-domain identifiers (mobile/CTV) land here** — AAO does not currently check them. |
Each bucket entry carries an `identifier: { type, value }` field that mirrors
the input — use it to disambiguate types.
`reportId` (plus `reportIds[]` when domain input is chunked) is returned only
when at least one `domain` was submitted. Bundles-only and store-IDs-only
requests return no report IDs since no AAO call was made.
## Cascade behavior
When you `PUT` a list with `domains` or `identifiers`, every active media buy that
uses the list is notified via ADCP `update_media_buy` so the sales agent can
refresh its cached property set. This covers include and exclude lists alike,
whether the list is one of the advertiser's lists or a campaign's own list.
Each campaign's media buys receive the combined reference in effect for that
campaign (see [Property-list briefs for sellers](/v2/storefront/property-list-briefs)).
Each reference goes only to a sales agent that declares its targeting
dimension (`property_list` for include, `property_list_exclude` for exclude),
the same rule as a new buy. A buy whose sales agent doesn't declare it is
updated without that reference, and the skipped change is logged. The update
response surfaces this with a `cascadeSummary`:
```json theme={null}
{
"cascadeSummary": {
"totalMediaBuys": 4,
"updatedCount": 3,
"failedCount": 1
}
}
```
The cascade is **best-effort**. Cascade failures do **not** roll back the
list update; the database is the source of truth and per-media-buy errors
are logged for retry. A `PUT` that sends neither `domains` nor
`identifiers` (a rename) does not cascade.
Per-media-buy fan-out is bounded (concurrency 5) so a large advertiser cannot
thunder a single sales agent.
## Resolution endpoint
Sales agents resolve a `PropertyListReference` by calling
`GET https://api.apostra.com/lists/:listId` with an HMAC bearer token
that Apostra mints when it embeds the reference in a request. This endpoint is
mounted at the **app root** (no `/api/v2/buyer` or `/api/v2/storefront` prefix) per ADCP convention. The
endpoint returns ADCP `GetPropertyListResponse` shape (not the standard
`{ data, error, meta }` envelope):
```json theme={null}
{
"list": { "list_id": "42", "name": "Cross-screen premium" },
"identifiers": [
{ "type": "domain", "value": "example-times.com" },
{ "type": "ios_bundle", "value": "com.exampletimes.ExampleTimes" },
{ "type": "android_package", "value": "com.exampletimes.android" }
],
"resolved_at": "2026-04-25T10:30:00.000Z",
"cache_valid_until": "2026-04-26T10:30:00.000Z"
}
```
Agents are expected to cache the response until `cache_valid_until` (24 h).
## Best practices
* **Always inspect `resolutionSummary`.** Any non-zero `unresolvedCount`
indicates identifiers that will not target — surface them to the user before
going live.
* **Submit multiple identifier types for the same app.** A mobile app is more
reliably resolved when you include both its `ios_bundle` and
`android_package` (and, where available, `apple_app_store_id` /
`google_play_id`). Each is resolved independently against the AAO registry.
* **Prefer one large list over many small ones.** A 100k-identifier list is
cheaper than 100 lists of 1k identifiers because each list creates its own
`SmartPropertyList` link.
* **Use `filters.channels_any` to scope a list.** Without it, the list applies
across all of the brand's targeting profiles. With it, you can keep
display/OLV separate from CTV.
* **Treat updates as full replacements.** `PUT` replaces the entire identifier
set; there is no incremental add/remove. Re-send the union you want stored.
* **Validate before bulk import.** Run `POST /api/v2/buyer/property-lists/check`
on the raw input first, especially when assembling a list from spreadsheets
or third-party feeds.
* **Avoid touching lists during high-traffic windows.** Cascading notifies
every active media buy that uses the list; schedule large updates for
off-peak times when possible.
## Limits
| Limit | Value |
| - | - |
| Identifiers per request (create / update / check) | 100,000 |
| Domains per AAO registry call (auto-chunked) | 10,000 |
| Cascade concurrency per update | 5 media buys at a time |
| List name length | 1–255 characters |
| Cache TTL on resolved list | 24 hours |
## Related
What a seller's agent receives and resolves when a buyer campaign carries a
property list.
How seller coverage appears in `get_products`.
# Reliable Reporting
Source: https://docs.apostra.com/v2/guides/reliable-reporting
How Apostra is making reporting status, delivery evidence, and billing reconciliation explicit.
## What Reliable Reporting changes
Reporting is most useful when you can answer three separate questions: what has
been reported, whether the report is complete for the scope you asked for, and
whether a later correction can change it. Reliable Reporting is the AdCP 3.2
model for answering those questions with an explicit record rather than a
guess based on how recently a source responded.
Reliable Reporting is in a controlled staging rollout. It is not yet available
through Apostra's public API, MCP server, or SDK. In particular,
`get_reporting_status`, reporting notifications, managed delivery, and
reporting receipts are not public features today. Use the existing [Reporting
overview](/v2/guides/reporting-overview) and `get_delivery` for current
delivery reads.
The terms on this page describe the contract being prepared for that rollout.
They do not change the behaviour of existing 3.0 and 3.1 reporting ingestion:
seller webhooks, polling, and bucket delivery continue to feed the current
day-grain reporting pipeline.
## Three levels of assurance
Reliable Reporting has three levels. Each level builds on the one before it.
| Level | What it establishes | Availability |
| - | - | - |
| Core | A scheduled reporting obligation, immutable report revisions, and an authoritative status read. | Controlled staging rollout; not public yet. |
| Managed delivery | A verified copy of a revision at a file, dataset-share, or warehouse destination. | Not public yet. |
| Reconciled billing | An authenticated consumer receipt for a verified delivery. | Not public yet. |
Core does not require a destination or receipt. Managed delivery adds evidence
that a consumer can read the delivered data. Reconciled billing adds the
consumer's acknowledgement, so billing can distinguish data that was produced
from data that was received and checked.
## The records behind a report
An **obligation** is created before the report is due. It fixes the reporting
period, schedule, required finality, selected media buys, and coverage rule for
that period. A later campaign or configuration change affects a future
obligation, not the one already recorded.
When data is ready, it becomes an immutable **revision**. A revision identifies
the report definition, period, coverage, finality, row count, and control
totals (the totals used to check the report). A zero-row revision is still a
report: it says that the source checked the period and found no rows.
A correction is a new revision, called a **restatement**. It links to the
revision it replaces; it never overwrites the earlier revision. That leaves a
reader able to see both what was first published and what superseded it.
## Status is an operational answer
The planned `get_reporting_status` read will provide one of five health states
for the requested scope:
| Status | Meaning |
| - | - |
| `waiting` | No report is due yet. |
| `healthy` | Every report currently due has been produced and has met the required delivery or reconciliation step. |
| `delayed` | A report is late, but automatic recovery is still running. |
| `action_required` | Automatic recovery has finished or a named party needs to act. |
| `complete` | The requested scope is closed and every required final report is satisfied. |
Coverage and finality remain separate. A fresh report can be partial; a complete
report can still be a snapshot; and an official report can later be restated.
The status read is designed to make those limits visible instead of treating a
recent timestamp as proof.
## Notifications and recovery
The planned notification types are compact doorbells, not report delivery:
* `reporting.status_changed` will signal a committed health change.
* `reporting.delivery_ready` will signal a verified managed delivery.
* `reporting.ledger_changed` will signal a receipt or other reconciliation change.
Notifications can be repeated, arrive out of order, or be missed. Consumers
will use `get_reporting_status` to recover the authoritative state. Apostra
will advertise a notification only when that notification and its signed
delivery path are available.
## What to use today
For delivery and pacing today, call `get_delivery` or the day-grain buyer
reporting endpoint. Treat them as seller-reported delivery. They do not expose
Reliable Reporting revisions, authoritative status, managed-delivery evidence,
or billing receipts. See [Reading reporting](/v2/buyer/reporting/overview) and
[Reporting overview](/v2/guides/reporting-overview).
For several sources or platforms under one campaign, see
[Multi-platform reporting](/v2/guides/multi-platform-reporting).
# Reporting Overview
Source: https://docs.apostra.com/v2/guides/reporting-overview
How v2 reporting hierarchies, metrics, time-series, and CSV exports fit together — plus how delivery data lands in the reporting pipeline in the first place.
## Overview
v2 reporting metrics are delivery numbers (impressions, spend, clicks, conversions, derived rates) rolled up across a hierarchical tree: **account → advertiser → campaign → media buy → package**. Every request hits a single endpoint and resolves the same tree before joining delivery metrics on top.
```
GET /api/v2/buyer/reporting/metrics
```
It returns either a **hierarchical summary** (advertiser → campaign → media buy
→ package) or a **time-series** (one row per leaf × day), and can also generate
a CSV with a 7-day signed download URL. Metrics come from the reporting
pipeline; the hierarchy and access control come from the system of record.
Delivery can enter that pipeline from seller webhooks, scheduled source polls,
or a storefront's modular final-report workflow. A modular final report appears
in buyer reporting when it includes an explicit currency and per-day
`date`/`impressions`/`spend` rows that reconcile to its final totals.
Aggregate-only modular reports still complete the seller's booking workflow but
are not assigned to a made-up reporting day.
**Hourly delivery reporting is not supported on `/api/v2/buyer/reporting/metrics`** — this endpoint is **day-grain only**. There is no `granularity` parameter; passing one will be ignored. For hourly **event** counts (conversions, clicks, impressions tracked via your own pixel/event sources), use `GET /api/v2/buyer/advertisers/:id/events/summary` instead.
v2 deliberately collapses what was a sprawl of v1 tools (`get_campaign_summary`,
`export_campaign_data`, `analyze_tactics`, etc.) into one endpoint with a
`view` switch. The same shape works for dashboards, agent prompts, and BI
exports.
## Murph seller analytics
Storefront operators can ask Murph for seller analytics. Those analytics combine
recent product-discovery intelligence runs with attributed commercial outcomes
recorded when a buyer creates or updates media buys.
The `sellerAnalytics.outcomes` block summarizes approval and delivery activity
attributed to recent runs:
* `attributedRunCount`, `eventCount`
* `submittedCount`, `forwardedCount`, `forwardFailedCount`, `rejectedCount`,
`deliveryReportedCount`
* `bookedMediaBuyCount`, `bookedBudget`, `deliveredImpressions`,
`deliveredSpend`, `deliveredCurrency`
Each recent run also includes an `outcome` object with the same counts plus the
latest attributed `latestType`, `latestStatus`, and `mediaBuyId`. Supported
event types are `media_buy_submitted_for_approval`, `media_buy_forwarded`,
`media_buy_forward_failed`, `media_buy_rejected`, and `delivery_reported`. See
the [Storefront object guide](/v2/object-guides/storefront#murph-selleranalytics)
for the field-level schema, including the `buyers[]` rollup that ranks buyer
domains by recent runs, asks, win rate, ask-to-book conversion, attributed
bookings, and delivery.
The same payload can include `historicalPerformance`, `strategySignals[]`, and
`seasonality[]`, and `sellerRecommendations[]`. Historical performance
summarizes win rate, ask-to-book conversion, price-objection conversion,
packaging conversion, booked budget, delivery rate, repeat buyers, and
product-selection breadth. `seasonality[]` buckets the same signals by UTC month
so sellers can see periods where demand, objections, or booking size changed.
`strategySignals[]` turns those metrics into directional negotiation postures
such as holding value, preserving value on price asks, using clearer package
ladders, or starting with fewer products. Posture values are documented in the
[Storefront object guide](/v2/object-guides/storefront#murph-selleranalytics)
and include `hold_value`, `value_preserving_compromise`, `tradeoff_ladder`,
`direct_fit`, `price_first`, and `wholesale_mirror`. `sellerRecommendations[]`
turns the same window into deterministic seller actions.
Outcome totals use last-touch product-overlap attribution: outcome events are
matched to the most recent intelligence run that surfaced the relevant product
IDs. Treat them as directional seller analytics rather than audited financial
reporting. Seller recommendations are directional coaching signals, not
automated pricing or packaging changes.
## The hierarchy
Every metrics request resolves the same four-level tree before joining delivery
metrics on top:
```
Account
└── Advertiser
└── Campaign
└── Media Buy
└── Package (one per product within the buy)
```
Filters cascade: `advertiserId` narrows to that advertiser, `campaignId` narrows to
one campaign within an advertiser, and the system enforces that the campaign
belongs to the advertiser when both are passed.
## Channel-group lineage
When a campaign uses [channel groups](/v2/buyer/campaigns/channel-groups), each
media buy keeps the group ID and name that compiled into it. REST reporting
returns that saved lineage in every view:
* A hierarchical summary returns `channelGroup: { channelGroupId, name }` on
each media buy.
* A time-series row returns `channelGroupId` and `channelGroupName`.
* A CSV export appends `Channel Group ID` and `Channel Group Name` columns.
Legacy or ungrouped buys return `null` in JSON and blank CSV fields.
Apostra does not infer a group from a media-buy name, product, or delivery.
REST callers can pass `channelGroupId` to filter the durable media-buy scope;
combine it with `advertiserId` or `campaignId` to narrow to their intersection.
For an agent-side rollup, call V3 `get_delivery` with
`report: "campaign_delivery"` and `dimensions: ["channel_group"]`. Add
`filters.channelGroupId` when you need one group only:
```json theme={null}
{
"report": "campaign_delivery",
"metrics": ["impressions", "spend", "clicks"],
"dimensions": ["channel_group"],
"range": {
"startDate": "2026-09-01",
"endDate": "2026-09-30"
},
"filters": {
"campaignId": "camp_001",
"channelGroupId": "mobile-web-display"
}
}
```
The selected dimension is returned as `channelGroup: { id, name }`, or `null`
for an ungrouped buy. The REST `/reporting/metrics` endpoint returns lineage
and accepts the group as a filter, but does not expose the V3 grouping
dimension.
## Group delivery by media channel
A channel group is a grouping you compose for a campaign. To see delivery by
the media channel of what actually ran, such as CTV, online video, or display,
call V3 `get_delivery` with `report: "campaign_delivery"` and
`dimensions: ["channel"]`:
```json theme={null}
{
"report": "campaign_delivery",
"metrics": ["impressions", "spend"],
"dimensions": ["channel"],
"range": {
"startDate": "2026-09-01",
"endDate": "2026-09-30"
}
}
```
Each row's `channel` is the list of AdCP media channels declared by the
product behind that package, for example `["ctv"]`. When the product itself
declares no channels, the row uses the channels its format options declare in
AdCP `applies_to_channels`, the same rule product discovery uses to match a
product to a channel. A seller that sells
several channels is therefore split by what each package bought, not
classified as a whole. Values use the AdCP `MediaChannel` vocabulary
(`display`, `olv`, `ctv`, `streaming_audio`, `dooh`, and so on).
When a product lists several channels, such as `["display", "olv"]`, the row
reports that whole list as one value rather than guessing which channel
delivered, so no delivery is counted twice.
A row returns `channel: null` when:
* **Neither the product nor its formats declare channels.** AdCP does not
require a seller to declare channels, so some sellers' products carry none.
A format kind such as `video` or `audio` on its own is not treated as a
channel.
* **The package has no product on record.**
* **The row covers a whole media buy rather than one package** and the buy's
packages do not all declare the same channels, or the buy has no packages.
The response summary says how many rows have no channel. The channel is read from the product's current
record each time you run the report, so if a seller changes a product's
channels, earlier delivery for that product moves with it. Channel is a
reporting axis only; it does not change targeting or billing.
## Available metrics
Every level reports the same metric block:
| Metric | Type | Notes |
| - | - | - |
| `impressions` | int | Sum across deduped daily reports |
| `spend` | number | Sum in the advertiser's primary currency (see [Currency](#currency)). Gross — fee-inclusive, stated at the fee terms locked on each media buy, so it compares directly against your budgets. Legacy media buys created before fee terms were locked report spend net, as the seller reported it. See [Budgets and fees](/v2/concepts/budgets-and-fees) |
| `clicks` | int | |
| `views` | int | Content views as the seller counts them: a platform-defined video view (for example 30 seconds or the midpoint), an audio stream start, or the view event defined by the package's pricing model. This is what CPV pricing bills on. It is not a count of viewable impressions; see [Viewability](#viewability). |
| `completedViews` | int | Video/audio completions (qualified by `view_duration_seconds` when set on the goal) |
| `conversions` | int | |
| `leads` | int | |
| `videoCompletions` | int | |
| `ecpm` | number \| null | `(spend × 1000) / impressions` |
| `cpc` | number \| null | `spend / clicks` |
| `ctr` | number \| null | `clicks / impressions` |
| `completionRate` | number \| null | `completedViews / impressions` |
Derived metrics are `null` when their denominator is zero.
### Viewability
Reporting does not include viewability yet. `views` counts content views, not viewable impressions, so don't divide `views` by `impressions` to estimate a viewability rate. A viewability rate is viewable impressions divided by *measurable* impressions (impressions where viewability could be measured), and the metrics above carry neither count.
## Currency
Every advertiser is locked to a single **primary currency**. Each summary
advertiser block carries a `currency` field naming it. If delivery arrives in a
different source currency, reporting uses the buy's booked FX rate or a
creation-date historical rate when one is available.
If a buy's spend cannot be denominated at all — the buy carries no evidence of
which currency its source is paid in, or its rows span currencies within the
period — summary requests return `SPEND_DENOMINATION_UNRESOLVED` (HTTP 422)
instead of returning money under the wrong currency label. The error names the
affected media buys in its message and in `details.mediaBuyIds`. It is classified
`terminal`: retrying reproduces it, because the data is wrong at rest. Scope the
request to exclude those buys to report on the rest of the account. Time-series
requests can remain available because each row can retain and declare its source
currency. See [Cross-currency](/v2/concepts/cross-currency) for the full rule.
Time-series rows carry the currency that actually denominates their money
alongside their advertiser ancestry. Normally this is the advertiser's primary
currency. If no FX rate is available, the row retains its source currency rather
than relabeling unconverted spend. Both summary and time-series responses expose
`totalsCurrency`. Dashboard, in-chat, and CSV views use those fields rather than
assuming USD; every CSV row appends a `Currency` column without shifting existing
CSV columns.
Money `totals` are exact only when every returned row or advertiser block shares
one currency; the response names it in `totalsCurrency`. Advertisers with no
returned time-series rows do not affect time-series `totalsCurrency`. When the
returned data spans currencies, the money metrics in `totals` (`spend`, `ecpm`,
`cpc`) are `null` — the platform never adds EUR and USD together as one number.
Count metrics and unitless rates stay summed.
For a single cross-currency figure, pass **`displayCurrency`** (summary view
only, any platform-supported ISO-4217 code). The response then includes a
`consolidated` block: each advertiser's money is converted at the current
rate-of-the-day and *then* summed, with a per-advertiser audit trail
(`byAdvertiser`) and a `conversionInfo` object recording exactly how the
figure was produced — the rates and pairs applied, the `asOf` day, and
`estimate: true`. When your advertisers already share one currency,
`displayCurrency` defaults to it and the consolidated block only appears when
you ask for a different currency.
Consolidated figures are display **estimates** converted at one current
rate-of-the-day — they are never used for billing, invoices, or settlement,
which always happen in native currencies. A currency that cannot be priced is
excluded from the converted total and listed in `conversionInfo.unconverted`
rather than guessed.
## Query parameters
| Param | Type | Default | Notes |
| - | - | - | - |
| `advertiserId` | string | — | Advertiser ID |
| `campaignId` | string | — | Must belong to `advertiserId` if both passed |
| `mediaBuyId` | string | — | Media buy ID |
| `channelGroupId` | string | — | Channel group ID |
| `startDate` | `YYYY-MM-DD` | derived from `days` | |
| `endDate` | `YYYY-MM-DD` | today | |
| `days` | int 0..90 | 7 | `0` = "all time" — resolves to the account's first reporting date |
| `view` | `summary` \| `timeseries` | `summary` | |
| `displayCurrency` | ISO-4217 | your home currency when resolvable | Consolidated FX-converted rollup (see [Currency](#currency)); summary view only |
| `download` | bool | `false` | When `true`, returns a signed CSV URL |
| `demo` | bool | `false` | Returns auto-generated demo data — useful for sandbox UIs |
Both summary and timeseries JSON responses honor the requested window up
to 90 days. For wider exports use `?download=true` to get a CSV.
## Hierarchical summary (`view=summary`)
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/reporting/metrics?days=14&advertiserId=42" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```json theme={null}
{
"advertisers": [
{
"advertiserId": "42",
"advertiserName": "Acme Corp",
"currency": "EUR",
"metrics": { "impressions": 12345, "spend": 678.9, "...": "..." },
"campaigns": [
{
"campaignId": "camp_001",
"campaignName": "Spring Awareness",
"metrics": { "...": "..." },
"mediaBuys": [
{
"mediaBuyId": "mb_abc",
"name": "CTV Always-On",
"channelGroup": {
"channelGroupId": "ctv",
"name": "CTV"
},
"status": "ACTIVE",
"budget": 50000,
"metrics": { "...": "..." },
"packages": [
{
"packageId": "pkg_xyz",
"productId": "prod_xyz",
"productName": "Premium CTV — Network Supply",
"metrics": { "...": "..." }
}
]
}
]
}
]
}
],
"totals": { "...": "..." },
"totalsCurrency": "EUR",
"periodStart": "2026-04-12",
"periodEnd": "2026-04-25"
}
```
## Time-series (`view=timeseries`)
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/reporting/metrics?view=timeseries&days=7" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Returns flat rows — one per leaf per day — sorted by date ascending. Every row
carries the full ancestry so a chart can group by any level without an extra
join:
```json theme={null}
{
"timeseries": [
{
"date": "2026-04-19",
"advertiserId": "42",
"advertiserName": "Acme Corp",
"currency": "EUR",
"campaignId": "camp_001",
"campaignName": "Spring Awareness",
"mediaBuyId": "mb_abc",
"mediaBuyName": "CTV Always-On",
"mediaBuyStatus": "ACTIVE",
"channelGroupId": "ctv",
"channelGroupName": "CTV",
"packageId": "pkg_xyz",
"productId": "prod_xyz",
"productName": "Premium CTV — Network Supply",
"metrics": { "...": "..." }
}
],
"totals": { "...": "..." },
"totalsCurrency": "EUR",
"periodStart": "2026-04-19",
"periodEnd": "2026-04-25"
}
```
## CSV export (`download=true`)
Append `download=true` to either view to generate a CSV in GCS and return a
signed URL. Each row includes the effective `Currency` and appends `Channel
Group ID` and `Channel Group Name` after the existing columns. Spend, eCPM, and
CPC remain self-describing outside the JSON response, and existing positional
columns retain their previous indexes:
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/reporting/metrics?view=timeseries&days=90&download=true" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```json theme={null}
{
"downloadUrl": "https://storage.googleapis.com/scope3-reporting-exports/...",
"expiresAt": "2026-05-02T00:00:00Z",
"fileName": "reporting-2026-01-25-2026-04-25.csv",
"rowCount": 1842
}
```
* The signed URL expires in **7 days**.
* CSV columns mirror the JSON shape (one row per leaf for summary, one row
per leaf × day for time-series).
* Download is the recommended path for ranges longer than 7 days when
`view=timeseries`.
**The signed download URL is itself a bearer credential.** Anyone who has
the URL can download the export — there is no additional auth check beyond
URL knowledge until expiry.
* Treat `downloadUrl` like a password or API key: never paste it into chat,
tickets, screenshots, public dashboards, or unencrypted email.
* Don't log the URL in long-lived application logs.
* Hand the URL to the consuming system over a secure channel and have that
system fetch immediately rather than queuing it for later.
* The export contains advertiser, campaign, and spend data — treat the
downloaded CSV itself as sensitive once retrieved.
## Audit logs (related)
Audit logs are a sibling read endpoint for the buyer activity feed:
```
GET /api/v2/buyer/audit-logs
```
The buyer feed surfaces meaningful actions on `CAMPAIGN`, `CREATIVE`,
`MEDIA_BUY`, and other resource types. Filterable by `startDate` / `endDate`,
`advertiserId`, `campaignId`, and `resourceTypes`. Returns up to 500 logs per
page (default 50) plus a `total` for pagination.
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/audit-logs?campaignId=camp_001&take=100" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Use audit logs for "who did what, when" questions — recorded in real time.
Use `/reporting/metrics` for performance numbers — aggregated nightly from
agent-reported delivery.
## How delivery data flows
This section describes third-party **SALES agents** — sell-side inventory
sources reached over AdCP (see [Storefront onboarding](/v2/setup/storefront-onboarding)).
It does not describe the direct ad-platform adapters (Meta, Google, TikTok,
and the rest): those adapters don't push webhook deliveries at all. Apostra
polls each one's own API instead — see the "Webhooks and async lifecycle"
section on any [ad platform compatibility](/v2/guides/ad-platform-compatibility)
page for what a specific adapter actually does.
Reporting metrics depend on the sales agent reporting actual delivery back to
Apostra. Three transports are supported and chosen at agent registration time
via the SALES agent's `reportingType` field. See the
[Storefront onboarding guide](/v2/setup/storefront-onboarding) for the
registration syntax — the reporting transports themselves follow the
[ADCP optimization & reporting spec](https://docs.adcontextprotocol.org/docs/media-buy/media-buys/optimization-reporting).
Agent posts ADCP `get_media_buy_delivery`-shaped payloads to an Apostra
webhook URL embedded in the original `create_media_buy` call. Best for
most integrations.
Agent writes JSON / JSONL / CSV / Parquet files to S3, GCS, or Azure Blob.
Apostra watches the path and ingests on landing. Best for high-volume
batch reporting.
Apostra calls the agent's `get_media_buy_delivery` on a `DAILY` or
`MONTHLY` schedule. Best for legacy systems without outbound webhooks.
All three transports land in the same reporting store and are deduped per
`(reporting_date, media_buy_id, source_id)`, keeping the most recent
`received_at` for that source. From there, `/reporting/metrics` does the math.
### Finality and revisions
Delivery can remain provisional after it has numbers. When Apostra has a
canonical reporting revision for a media buy, `get_media_buy_delivery` returns
that revision, including its `data_through`; an `OFFICIAL` revision makes the
buy's `is_final` true and carries its `finalized_at`. Without canonical evidence,
the response never invents a revision or finalization. For a routed buy that
falls back to source-provided finality, `is_final` is true only if every leg
reported finality, and the returned `finalized_at` is the latest leg timestamp.
### Webhook delivery
When a SALES agent is registered with `reportingType: WEBHOOK`, Apostra issues
a per-media-buy webhook URL inside the original `create_media_buy` call. The
agent posts ADCP `get_media_buy_delivery`-shaped payloads back to that URL on
its own cadence. Every request is signed and timestamped:
| Header | Purpose |
| - | - |
| `X-ADCP-Signature` | HMAC-SHA256 of `timestamp + "." + raw-body`, hex-encoded, computed with the shared webhook secret. |
| `X-ADCP-Timestamp` | Unix time in seconds. Apostra rejects timestamps more than **5 minutes** from its clock to limit replay. |
**Operational rules**:
* The shared secret is provisioned at agent registration and stored encrypted
at rest alongside the agent's auth credentials. Treat it as
production-grade secret material — never log, commit, or paste it.
* Verify the signature in **constant time** before parsing the body. Reject
unsigned or mistimed requests. Retries are safe because reporting dedupes by
reporting day, media buy, and source.
* Rotate the secret on a documented schedule and on any suspected exposure.
Apostra supports a brief overlap window where both old and new secrets
validate so in-flight deliveries are not lost.
See the [Storefront onboarding guide](/v2/setup/storefront-onboarding)
for the registration syntax and matching rules on the agent side.
If a seller source is re-registered or migrated after a media buy starts, keep
posting delivery to the callback URL issued with that buy when the request
carries the seller's RFC 9421 signature. Apostra verifies
the retired callback identity against either the exact active seller source in
its archived route binding or one unambiguous active successor, and verifies
that each reported media-buy ID maps to exactly one buyer-owned route. Buyers
therefore continue receiving daily reporting without a callback change.
Shared-secret HMAC does not identify a retired seller and cannot authorize this
bridge. A callback that cannot be tied to one eligible active seller source and
unambiguous buyer routes returns `410 Gone`; use the callback from the current
media-buy request or contact support rather than rewriting the URL. Apostra
returns `200` only after the bridged reporting write is accepted; retry a `503`
response, and correct a report that receives `422` before sending it again.
For a previously issued callback that now returns `410`, use this cutover
checklist:
1. Keep the callback URL byte-for-byte unchanged, including every path ID and
the operation ID.
2. Publish a `jwks_uri` in the `brand.json` entry for the active registered
seller endpoint bound to the retired callback. Contact support if that
binding is unclear; do not guess or repoint the callback.
3. Sign the POST with RFC 9421 `Signature` and `Signature-Input` headers. Sign
the registered callback URL as `@target-uri`; do not sign a rewritten proxy
host. Shared-secret HMAC cannot authorize a retired callback.
4. Send the normal `media_buy_delivery` payload with the original upstream
`media_buy_id` and package IDs. Retry `503`; stop and correct `401`, `410`,
or `422` before retrying.
5. After the first `200`, replay each missing reporting day in date order. Keep
the same media-buy and package IDs; use a new monotonically increasing
`sequence_number` for each replayed notification.
See [Signing the webhooks you send us](/v2/storefront/inventory-sources/webhook-signing#rfc-9421-message-signatures)
for the full signing contract.
## Access control
* The account ID is taken from the auth context — buyers can only read metrics
for media buys their account owns.
* When `advertiserId` is provided, the platform double-checks advertiser access
before issuing any metrics query.
* Every metrics query also filters on `customer_id` at the data layer, so a
missing ACL entry cannot leak data.
## Demo mode
Pass `?demo=true` to receive the `synthetic-rolling-delivery-v1` fixture. It is
generated locally from UTC calendar dates: it does not read a provider account,
need provider credentials, create spend, or use your reporting history. It is
useful for demos, sandbox screens, and teaching an agent the reporting shape
before it makes a live query. Demo data is capped to a 90-day window.
**Synthetic demo data is unsuitable for commercial decisions.** Do not use it
for buying, billing, optimisation, forecasting, or performance claims.
The scenario has non-zero USD delivery for three labelled campaigns: one
improves, one declines, and one remains stable. For a fixed date window, it
returns the same leaf rows and totals every time. Adjacent equal-length windows
therefore show the improving, declining, and stable comparison without relying
on a live provider.
`advertiserId`, `campaignId`, `mediaBuyId`, and `channelGroupId` select the
same leaf hierarchy in summary and time-series responses. Start with an
unfiltered demo request to discover the fixture IDs, then apply those IDs to
either view. Summary totals roll up the selected packages and media buys;
time-series totals aggregate the same leaves by day.
Every JSON response includes a `synthetic` object with the scenario ID and the
commercial-use warning. Download responses carry that same object, use a
`synthetic-demo-reporting-…csv` filename, and attach the scenario ID and notice
to the exported object's metadata. The CSV URL is still a bearer credential;
handle it as described in [CSV export](#csv-export-downloadtrue).
# Service Tokens
Source: https://docs.apostra.com/v2/guides/service-tokens
Create, rotate, and revoke long-lived API tokens for service-to-service integrations
## Overview
**Service tokens** are legacy long-lived API tokens scoped to an account (all of its
advertisers) or a single advertiser. Existing tokens continue to work and remain
revocable during migration. For a new headless integration, create a dedicated service
user with the intended WorkOS access and issue a user API key as that user. Use M2M
when the integration specifically requires OAuth client credentials and a non-user
machine identity.
Each customer-owned service token also establishes a **workload identity** for
the software using that credential. `workloadUid` identifies the workload;
`credentialVersion` identifies the exact credential; and `adcpCallerUri` is the
stable caller identity used for AdCP governance delegation. Internal Apostra
system tokens do not receive buyer workload identities.
They are distinct from user API keys, which belong to an individual or dedicated
service user. Service tokens survive user offboarding and can be scoped down to a
single advertiser with a specific role, but they are a compatibility credential rather
than the default for new integrations.
New user API keys do not have a separate read/write/admin selector. Apostra assigns
API access automatically, then applies the owner's current account, advertiser, and
resource authorization on every request. To narrow a dedicated service user's key,
change that user's authorization in WorkOS rather than selecting a second key role.
| User API key | Legacy service token |
| - | - |
| Tied to a human or dedicated service user in one organization | Tied to an account or advertiser |
| Follows that user's current authorization | Survives user changes |
| Multiple least-privilege keys per user | Many per account |
| Default for: scripts and headless integrations | Compatibility only for existing integrations |
Both are bearer credentials — every request sends `Authorization: Bearer scope3_`.
### Token format
A service token is a single opaque string with three parts joined by underscores:
```
scope3__
```
* The `scope3_` prefix lets us recognize the token at the edge.
* `accessClientId` is a non-secret identifier for this credential version — safe to log, surface in admin UIs, and reference in audit trails.
* `accessClientSecret` is the credential — only shown once, at creation.
The full token (`fullToken` field on the create response) is shown **exactly
once**, on the response to `POST /service-tokens`. We store only a hash; we
cannot show it to you again. If you lose it, revoke the token and create a
new one.
## Prerequisites
You need an existing credential whose owner may manage tokens — a user API key from
[app.apostra.com/user-api-keys](https://app.apostra.com/user-api-keys),
or another service token with `ADMIN` role.
Pick `CUSTOMER` (acts across every advertiser) or `ADVERTISER` (limited
to one advertiser). For advertiser-scoped tokens, you also choose a role
(`ADMIN`, `READ_WRITE`, `READ`) and an expiry between 1 and 365 days.
Plan where you'll store `fullToken`. Service tokens act on your
organization's behalf — keep them in Google Secret Manager, AWS Secrets
Manager, HashiCorp Vault, or equivalent. Never commit them to git.
All examples below use:
```
BASE = https://api.apostra.com/api/v2
AUTH = Authorization: Bearer scope3_
```
Service-token endpoints live under `/api/v2/service-tokens`, not under
`/api/v2/buyer/...` or `/api/v2/storefront/...`. They are mounted on the v2
shared router because the same token-management surface works for buyer
and storefront accounts alike. Most other v2 endpoints sit under a
buyer/storefront mount — service tokens are the exception.
## Step 1: Create a service token
`POST /service-tokens` returns the new token, including the one-time-only
`fullToken` field. Capture it before doing anything else.
```bash Account-scoped theme={null}
curl -X POST "$BASE/service-tokens" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"name": "Nightly ETL — Snowflake export",
"description": "Pulls reporting data into the warehouse every 02:00 UTC",
"expiresInDays": 365,
"scope": "CUSTOMER"
}'
```
```bash Advertiser-scoped theme={null}
curl -X POST "$BASE/service-tokens" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme — campaign automation",
"scope": "ADVERTISER",
"advertiserName": "Acme Co.",
"role": "READ_WRITE",
"expiresInDays": 90
}'
```
```json Response theme={null}
{
"id": "12345",
"customerId": "cust_abc",
"advertiserId": null,
"userId": null,
"name": "Nightly ETL — Snowflake export",
"description": "Pulls reporting data into the warehouse every 02:00 UTC",
"accessClientId": "ak_01HXYZ...",
"fullToken": "scope3_ak_01HXYZ..._sk_live_9c8d7e6f...",
"expiresAt": "2027-04-26T00:00:00.000Z",
"createdAt": "2026-04-26T16:30:00.000Z",
"role": null,
"createdByUserId": "98765",
"workloadUid": "4f7d713a-94a5-4f64-9d55-13f99bb2dc42",
"credentialVersion": 1,
"replacesServiceTokenId": null,
"adcpCallerUri": "https://api.apostra.com/api/v2/buyer/workloads/4f7d713a-94a5-4f64-9d55-13f99bb2dc42"
}
```
Capture `fullToken` and store it in your secret manager **immediately**.
After this response, only `accessClientId` is retrievable.
### Request body
| Field | Type | Required | Notes |
| - | - | - | - |
| `name` | string | no | Human-readable label, max 255 chars |
| `description` | string | no | Free-form, max 1000 chars |
| `expiresInDays` | integer | conditional | 1–365. Required when `scope: ADVERTISER` |
| `scope` | enum | no | `CUSTOMER` (default) or `ADVERTISER` |
| `advertiserName` | string | conditional | Required when `scope: ADVERTISER` |
| `role` | enum | conditional | `ADMIN`, `READ_WRITE`, or `READ`. Required when `scope: ADVERTISER` |
### Selecting an advertiser per request
Account-scoped (`CUSTOMER`) tokens and user API keys select the advertiser to
act on by sending the `x-scope3-seat-id` request header with the numeric
advertiser ID:
```bash theme={null}
curl "$BASE/campaigns" \
-H "Authorization: Bearer scope3_" \
-H "x-scope3-seat-id: 88421"
```
The header name intentionally keeps its legacy `seat` spelling — it is a
stable wire contract that predates the seat→advertiser rename. The value it
carries is the advertiser ID. Advertiser-scoped tokens don't need it: they
are scoped to one advertiser at creation, and a header naming a different
advertiser is rejected with `403`.
## Step 2: List or fetch existing tokens
Use the list endpoint to see what tokens exist for your account (or for a
specific advertiser). The list view never includes the secret — only
`accessClientId`, workload and credential identity, scope, expiry, and audit
metadata.
```bash theme={null}
curl "$BASE/service-tokens?take=50&skip=0" \
-H "Authorization: Bearer scope3_"
```
| Query param | Type | Default | Notes |
| - | - | - | - |
| `advertiserId` | string | — | Filter by advertiser |
| `includeArchived` | boolean | `false` | Include revoked tokens |
| `take` | integer | `50` | Page size, max 100 |
| `skip` | integer | `0` | Offset |
To inspect a single token (for example, before extending its expiry):
```bash theme={null}
curl "$BASE/service-tokens/12345" \
-H "Authorization: Bearer scope3_"
```
You can also rename a token or **extend** its expiry through the update
endpoint. You cannot shorten the expiry through this endpoint — to
invalidate a token sooner, archive it (Step 4).
```bash theme={null}
curl -X PUT "$BASE/service-tokens/12345" \
-H "Authorization: Bearer scope3_" \
-H "Content-Type: application/json" \
-d '{
"name": "Nightly ETL — Snowflake export (renamed)",
"expiresInDays": 365
}'
```
## Step 3: Rotate a token
Treat service tokens like any other long-lived credential and rotate on a
schedule. The pattern is "create new, deploy, revoke old" — never edit the
existing token in place.
`POST /service-tokens` creates a new workload identity, so the manual rotation
below starts a new `workloadUid`; retain the old and new ids in your deployment
record so their activity can be audited separately. A registered buyer-agent API
key is the exception: rotate it through
[`POST /buyer-agent-principals/rotate`](/v2/setup/buyer-agent-credentials#api-key)
so the server preserves verified workload lineage and returns the replacement
secret once. Do not create a successor for that workflow through this endpoint.
`POST /service-tokens` with the same scope and role as the old one.
Capture `fullToken` and `id` from the response.
Update your secret manager and roll the new token out to every service
that holds the old one.
Wait until you've confirmed all consumers have switched — for example,
by checking deploy status, draining staging traffic, or watching error
rates after a forced restart. Don't revoke the old token until you're
sure nothing still depends on it.
`DELETE /service-tokens/`. Any service still using the old
token will start receiving `401 Unauthorized`, which is your final
signal that rollout missed somewhere.
Set `expiresInDays` to a value shorter than your rotation window so an
unrotated token expires on its own — defense-in-depth for the case where
a token is forgotten.
## Step 4: Revoke a token
Revocation is a soft delete — the token stops authenticating immediately,
but its audit row is preserved.
```bash theme={null}
curl -X DELETE "$BASE/service-tokens/12345" \
-H "Authorization: Bearer scope3_"
```
Returns `204 No Content`.
**Rotate on suspected leak.** If a token might have been exposed
(CI log, shared screen, leaked dependency), revoke it via
`DELETE /service-tokens/:id` immediately, then issue a replacement.
Do not wait for confirmation.
## Best practices
* **Use advertiser scope when possible.** An `ADVERTISER`-scoped token with `READ` role
cannot move spend, even if exfiltrated. Default to least-privilege: pick
the narrowest scope and lowest role that lets the integration do its job.
* **One token per workload.** If two services have different lifecycles or
different blast radii, give them separate tokens. That way a leak or
rotation only affects one workload at a time.
* **Treat as a credential.** Never commit tokens to git, never paste them
into chat or email, never hard-code them in client-side bundles.
* **Multi-tenant tools have two patterns.**
* *One token per managed advertiser.* Create an `ADVERTISER`-scoped token
per managed advertiser with the minimum role each integration needs.
Best when the advertisers have different administrators or you want
clean per-tenant audit trails.
* *One account-scoped token.* Use `scope: CUSTOMER` and rely on
per-request advertiser selection. Best when a single team owns
automation across every managed advertiser.
* Either way, never share a single token across unrelated tenants — it
conflates audit trails and forces a global revocation if any tenant is
compromised.
## Endpoint reference
All paths are relative to `https://api.apostra.com/api/v2`.
| Method | Path | Purpose |
| - | - | - |
| `GET` | `/service-tokens` | List service tokens |
| `POST` | `/service-tokens` | Create a service token (returns `fullToken` once) |
| `GET` | `/service-tokens/:id` | Get a single service token |
| `PUT` | `/service-tokens/:id` | Update name, description, or extend expiry |
| `DELETE` | `/service-tokens/:id` | Archive (revoke) a service token |
## Related
Bearer token format, header conventions, and how personal API keys
differ from service tokens.
Status codes and JSON error shapes returned by the service token
endpoints — including auth and validation failures.
# Advertiser
Source: https://docs.apostra.com/v2/object-guides/advertiser
The brand-and-operator identity that owns campaigns, creatives, audiences, and linked partner accounts
## Overview
An **Advertiser** in Apostra is the commercial media identity on whose
behalf campaigns run: a **brand care of an operator**. The brand answers whose
products are being advertised; the operator answers which organization drives
the buying. This is an Apostra product projection, not an assertion that
AdCP collapses the two entities: AdCP keeps Brand, Operator, Account, and Agent
separate.
For buyer-declared AdCP accounts, the natural key combines the BrandKey
(including its country qualifiers), operator domain, optional operator-unit ID,
fixed currency, and sandbox mode.
An upstream-managed `account_id` instead
names an entry in the seller's own namespace, whose declared `account_scope`
may be operator-, brand-, operator-and-brand-, or agent-wide. See
[AdCP Accounts and agents](https://docs.adcontextprotocol.org/docs/building/by-layer/L2/accounts-and-agents).
Direct buying is the identity case where the brand operates its own seat. For
example, Coca-Cola buying directly from an account registered to
`coca-cola.com` is "Coca-Cola care of Coca-Cola"; Coca-Cola through WPP is
"Coca-Cola care of WPP." **Self-serve is an interface, not an operator
identity**: using Apostra directly does not replace Coca-Cola with
"self-serve" as the operator. Likewise, an agent is the software acting for the
operator, not the operator itself. When a buyer account has no registered
domain, current v2 outbound AdCP calls use `interchange.io` as a platform
operator fallback until the account records its real domain; that fallback is
an implementation compromise, not the meaning of self-serve.
In the current Apostra v2 buyer API, an advertiser is also the top-level resource
container for media. Every campaign, creative, audience, frequency cap, and
linked partner account belongs to one. The authenticated buyer account
establishes the operator context, so the v2 advertiser payload does not repeat
an `operator` field.
## Media Company-managed advertisers
When a Media Company adds an advertiser for a campaign it will operate on its
own inventory, the Media Company remains the operator and the advertiser is the
brand it represents. Apostra binds that advertiser to the organization's
exact Storefront; the operator does not create a seller connection or switch
into a separate Buyer account.
The operator can select known authorized Products directly or ask for a
proposal when it wants merchandising recommendations. For an exact own-supply
campaign, uploading Creative records the operator's Storefront creative
decision, and confirming launch records its Storefront media-buy decision.
Those decisions do not bypass ad-server creative moderation, validation,
readiness, financial, or safety checks. External and sponsored buyers continue
to follow the Storefront's configured approval settings.
**Renamed from Brand Agent**: In v1 this concept was called a "Brand Agent".
In v2 it has been renamed to **Advertiser** to align with industry terminology
used by Google Ads, The Trade Desk, DV360, and Amazon DSP.
## Why advertisers matter
* **Resource ownership** — campaigns, creatives, audiences, and frequency caps all roll up to a single advertiser
* **Commercial identity** — every advertiser combines a brand reference with the operator acting for it
* **Brand identity** — every advertiser links to a brand resolved from the AdCP brand registry (or a `/.well-known/brand.json` document on the brand's domain)
* **Default policies** — `optimizationApplyMode`, UTM parameters, and frequency caps set on the advertiser flow down to its campaigns
* **Sandbox isolation** — sandbox advertisers route every ADCP call to test accounts, so you can rehearse end-to-end flows without real spend
* **Linked accounts** — connect the advertiser to existing accounts on upstream platforms (e.g. Snap, programmatic DSPs) for cross-platform reporting and execution
* **Channel preferences** — record the AdCP media channels the advertiser expects to use; overlap affects automatic seller qualification without creating advertiser-owned selection edges
* **Reporting time-zone preference** — record the buyer's desired reporting boundary while allowing sellers to use a fixed or assigned effective time zone
## Key fields
`labels` is the Buyer Account's optional dimension-keyed classification for the
advertiser. It is managed through the V3 MCP `save_advertiser` tool and read
through V3 `get` and `search`; see [Dimensions and labels](/v2/object-guides/dimension).
The advertiser resource has two response shapes:
* **Summary** — returned by `GET /api/v2/buyer/advertisers` (list). A small, fixed set of row-level fields.
* **Full** — returned by `GET /api/v2/buyer/advertisers/:id` (get). The complete resource with nested brand, linked accounts, UTM config, and frequency caps.
### Organization roster projection
`GET /api/v2/organization/advertisers` returns one bounded advertiser roster
over the compatibility accounts the authenticated organization context may
read. It does not copy advertisers or campaigns: every entry points back to the
existing `advertiserId` and compatibility-account boundary.
Owned entries use `relationshipClass: "owned"`. A plain advertiser row is
`operatingMode: "managed"`; an advertiser backed by the existing confined
seller-buyer sponsorship is `operatingMode: "self_serve"` and
`operatorRelationship: "client"`.
An accepted, unexpired organization advertiser grant emits only its named
advertiser IDs as `relationshipClass: "delegated"`. The owner keeps the
advertiser, Campaign IDs, and backing account. The grantee receives the grant
reference, owner organization, reviewed capabilities, and expiry, but never
the owner's account ID, members, billing, unrelated advertisers, future
advertisers, or private configuration. See
[Manage organization advertiser grants](/v2/buyer/advertisers/tasks/manage-organization-advertiser-grants).
This limited-beta projection also requires an active beta entitlement and
server-side beta exposure enabled by Apostra. Each delegated roster read
rechecks an accepted, unexpired grant for the exact advertiser and
`advertiser.read` capability.
Counterparty AdCP relationship
reconciliation is dark prerequisite infrastructure: no customer or internal
account is enrolled, and the public API does not make counterparty entries or
repair available from this slice. Later activation work must separately enable
both the fail-closed customer flag and runtime entitlement before any
counterparty row can appear.
Each resolved row includes binding provenance and resolution state,
caller-relative management capability, and readiness. Pending, suspended, and
disabled advertiser rows remain visible with non-ready state instead of being
silently treated as active. Resolved rows deduplicate only by canonical
advertiser ID; matching names or brand domains never merge advertisers or widen
access.
When later activation authorizes counterparty entries, exact duplicate tuples
including the explicit operating mode deduplicate deterministically in the
resolved entries collection, while relationships that differ only by operating
mode remain distinct. Incomplete, stale, revoked, or conflicting relationships
remain separately visible in the additive unresolved counterparty collection
until a direct-human Seller-admin performs the explicit entitlement-gated
repair.
The endpoint uses authenticated account membership and active explicit grants.
It never expands access from a name, email domain, CRM association, signup
history, provider, external Business Manager, or Partner status.
Advertiser-scoped credentials cannot read the organization roster.
### Seller-managed advertiser viewer access
When Apostra enrolls a seller-managed advertiser user, the user's current
seller, advertiser, role, and capability grant are checked again on every
request. A user with the `read` capability can list Campaign summaries through
`GET /api/v2/buyer/campaigns` only for that exact advertiser. The
`advertiserId` query parameter is optional, but when supplied it must match the
advertiser already selected at sign-in; it cannot switch advertisers or widen
access.
This access does not create a buyer account or customer membership. It does
not grant Campaign detail, Campaign changes or execution, reporting, other
advertiser routes, organization or member settings, or billing and payment
access. Missing, stale, suspended, or revoked grants are denied before Campaign
data is read. Users who need another advertiser must sign in with a separate
explicit grant for that advertiser.
### Summary shape (list rows)
| Field | Type | Notes |
| - | - | - |
| `id` | string | Stable advertiser identifier |
| `name` | string | Human-readable name (max 255 chars) |
| `status` | `ACTIVE` \| `ARCHIVED` | Soft-delete state |
| `sandbox` | boolean | When true, all ADCP calls route to sandbox accounts. **Immutable after creation.** |
| `brand` | string | Brand domain (e.g. `northwind.example`) or brand website URL |
| `brandCountries` | array | ISO 3166-1 alpha-2 qualifiers for this advertiser's BrandKey market scope. Empty means all countries |
| `brandCountriesKnown` | boolean | `false` only for a legacy advertiser whose empty country list is still unknown; otherwise an empty list is global |
| `primaryCurrency` | string | ISO 4217 currency used by campaigns under this advertiser |
| `preferredTimezone` | string \| null | Preferred IANA reporting time zone |
| `currencyLocked` | boolean | Whether currency can still change through the raw v2 API |
| `reportingTimezoneLocked` | boolean | Whether the reporting time-zone preference can still change |
| `channels` | array | Editable AdCP media-channel planning preferences; overlap affects automatic matching without owning seller selection |
| `linkedAccountCount` | integer | Number of partner accounts linked to this advertiser. Use `get_advertiser` for the full list. |
| `createdAt` / `updatedAt` | datetime | ISO 8601 timestamps |
### Full shape (single advertiser)
The single-advertiser endpoint returns every field above plus:
| Field | Type | Notes |
| - | - | - |
| `description` | string | Optional description (max 1000 chars) |
| `linkedBrand` | object | Resolved brand identity — name, domain, logos, colors, fonts, manifest |
| `optimizationApplyMode` | `AUTO` \| `MANUAL` | Default for the advertiser's campaigns. `MANUAL` requires approval before applying optimizations. |
| `linkedAccounts` | array | Partner accounts linked to this advertiser (e.g. Snap account, agency seats) |
| `utmConfig` | array | Click-URL parameters appended to landing-page URLs at delivery. The v2 name for V3 `tracking.macroAdditions`; a new advertiser has none |
| `frequencyCaps` | array | Buyer-side caps enforced by Apostra across all publishers |
| `brandWarning` | string | Warning about brand resolution (e.g. manifest not found) |
## Brand resolution
Advertisers do not store brand identity directly. Instead, they reference a **brand domain or website URL**, and Apostra resolves the brand identity from one of three sources:
1. The advertiser's `/.well-known/brand.json` (per the AdCP spec)
2. The AdCP brand registry
3. Brandfetch enrichment
Pass `brand: "northwind.example"` and Apostra fetches the resolved brand identity, surfacing logos, colors, fonts, tone, and product catalog under `linkedBrand` when available. If the advertiser is created or updated from enrichment rather than an official registry entry, the response includes `brandWarning`; do not retry create for that case. After reviewing the enriched details, set `saveBrand: true` on an update if you want to persist the brand in the registry. If no enrichment data is available, retry create with `saveBrand: true` only after confirming the advertiser name and brand domain.
See the [Brand object guide](/v2/object-guides/brand-story) for the brand
resolution model in detail.
## Uniqueness
The AdCP-aligned advertiser identity is BrandKey (domain, optional brand ID,
and sorted country qualifiers) × operator domain × optional operator-unit ID ×
fixed currency × sandbox mode. The same brand domain can therefore back more
than one advertiser resource when its brand ID, countries, or currency differ.
Those rows are separate campaign and billing containers, not different operator
identities. An AdCP 3.2 seller receives the whole BrandKey in `brand`:
`domain`, optional `brand_id`, and `countries` as sorted ISO 3166-1 alpha-2
codes, because AdCP 3.2 includes the countries in the account key. A seller on
AdCP 3.1 or earlier never receives `countries`, because its BrandRef has no
such field; for that seller the country scope stays inside Apostra. A global
scope sends no `countries`, since AdCP defines an omitted list as the
global or default identity.
An advertiser carries a brand ID only when the brand publishes it: the brand's
`/.well-known/brand.json`, or a registry entry the brand owner verified, must
list that ID. A brand ID that appears only in community or enriched brand data
is not stored, and the advertiser uses the domain alone. Apostra rechecks each
stored brand ID against the brand's registry entry about once a week. If the
brand later stops listing an ID that sellers already know, Apostra keeps sending
it so the advertiser's existing seller accounts stay intact, and flags the
mismatch to Scope3 operations.
An explicitly empty country list is a confirmed global BrandKey scope. The
country dimension does not block automatic matching in any declared seller
country; channel overlap and the seller's marketplace, transaction, and
authority rules still apply. It does not mean the advertiser has zero markets.
This differs from a legacy advertiser with
`brandCountriesKnown: false`, whose empty response list means the scope is still
unknown and must be classified before seller activation.
The buyer operator domain and unit are confirmed in Buyer Setup and remain
editable until the first advertiser is created. Advertiser creation is the
first-use boundary and freezes that account-level operator identity. Later
changes require identity migration rather than an inline edit on the advertiser.
Within a buyer account and sandbox mode, creating a second resource with the
same brand domain, optional brand ID, BrandKey countries, and currency returns
a conflict. Sandbox and non-sandbox advertisers are tracked separately.
Only one **active** sandbox advertiser can hold a brand identity at a time. An
archived sandbox advertiser releases its brand identity, so you can create a
new sandbox advertiser for the same brand, countries, and currency. An archived
non-sandbox advertiser keeps its brand identity: restore it instead of creating
a replacement.
## Sub-resources
The advertiser is the hub for many sub-resource collections. Each lives under `/api/v2/buyer/advertisers/:advertiserId/...` and inherits the advertiser's auth, brand, and sandbox context.
| Collection | Endpoint root | Purpose |
| - | - | - |
| **Campaigns** | `/api/v2/buyer/campaigns?advertiserId=…` | Marketing initiatives — flight dates, budget, products, creatives ([guide](/v2/object-guides/campaign)) |
| **Linked accounts** | `/advertisers/:id/accounts` | Existing accounts on upstream platforms (Snap, programmatic DSPs, agency seats) — see [below](#linked-accounts) |
| **Catalogs** | `/advertisers/:id/catalogs` | Product / offering / inventory feeds pushed via ADCP `sync_catalogs` and reused by discovery and creative generation |
| **Audiences** | `/advertisers/:id/audiences` | First-party audiences synced via ADCP for targeting and incrementality test cohorts |
| **Property lists** | `/advertisers/:id/property-lists` | Domain/email allow-and-block lists (up to 100k entries) ([guide](/v2/guides/property-lists)) |
| **Event sources** | `/advertisers/:id/event-sources` | Conversion event source registration ([measurement guide](/v2/guides/measurement-incrementality)) |
| **Measurement config** | `/advertisers/:id/measurement-config` | MMM, incrementality, and brand-lift configuration ([measurement guide](/v2/guides/measurement-incrementality)) |
| **Test cohorts / plans** | `/advertisers/:id/test-cohorts`, `/advertisers/:id/test-plans/:id` | Incrementality test scaffolding ([measurement guide](/v2/guides/measurement-incrementality)) |
| **Hypotheses** | `/advertisers/:id/hypotheses` | A/B test hypotheses |
| **Belief state** | `/advertisers/:id/belief-state` | Bayesian belief state from the measurement engine ([measurement guide](/v2/guides/measurement-incrementality)) |
| **Tracking config** | `/advertisers/:id/tracking-config` | Tracking macro and pixel template configuration |
| **UTM config** | inline on the advertiser | Click-URL parameters appended to landing-page URLs (set via `utmConfig` on create/update; see [Click-URL parameters](#click-url-parameters)) |
| **Frequency caps** | inline on the advertiser | Buyer-side caps that flow down to all of the advertiser's campaigns ([guide](/v2/guides/frequency-caps)) |
| **Allocations** | `/advertisers/:id/allocations` | Budget allocation entries used by the optimizer |
| **Event summary** | `/advertisers/:id/events/summary` | Aggregated conversion event metrics |
| **Syndication** | `/advertisers/:id/syndicate` | Syndicate first-party audiences to partner platforms |
| **Log event** | `/advertisers/:id/log-event` | Custom event ingestion ([guide](/v2/guides/log-events)) |
See the [Buyer API Reference](/v2/buyer-api-reference) for the full request and response shape of each.
## Tracker defaults and campaign inheritance
Third-party impression, click, and custom-event trackers are resources with stable
`tracker_id` values. Configure advertiser defaults at
`/advertisers/:advertiserId/tracking-config`; every campaign under that
advertiser resolves those defaults when the tracking-config resource is read.
Campaigns do not copy the advertiser array, so an advertiser update appears in
the next resolved API response immediately.
Configure campaign additions and inherited enable/disable decisions at
`/campaigns/:campaignId/tracking-config`. The campaign stores only its local
`custom_tracker_urls` and `tracker_overrides`. Its response also returns the
fully resolved `effective_trackers`, including `source_scope`, `inherited`,
`effective_enabled`, and `status_reason`, plus separate advertiser and campaign
revisions. An override whose advertiser tracker was removed is retained and
reported in `orphaned_override_ids` instead of silently targeting a different
tracker.
Each imported tracker retains both forms of its URL:
* `raw_url` is the exact vendor tag supplied by the buyer.
* `url` is the canonical form using [AdCP universal macros](https://docs.adcontextprotocol.org/docs/creative/universal-macros).
* `mapping_artifact` records the translation and documentation provenance.
* `source_dialect` and `compiler_version` make the transformation reproducible.
Strict V3 writes and publishable tracker compilation limit URLs to 8,192
characters and 64 macro tokens. The V2 compatibility surface may retain a
longer legacy URL exactly as non-publishable audit data so existing REST
read-to-write flows do not break; it cannot be published through V3 until it is
shortened. A publishable `CUSTOM` tracker must include `custom_event_name`;
strict V3 writes enforce that requirement. The V2 field remains optional so the
original REST request shape can round-trip, but an unnamed custom event is
recorded as non-publishable in its mapping artifact. The compiler bounds apply
before translation so malformed tags cannot amplify their mapping artifacts.
Strict V3 macro translation fails closed when a token is unknown or ambiguous.
The V2 REST compatibility surface continues to accept its original safe-URL
shape; an unresolved token is retained exactly and its mapping artifact is
marked non-publishable rather than guessed. For example, HappyDemics documents
`{{USER_ID}}` as the mobile advertising device
identifier, so a tracker explicitly identified as HappyDemics maps that token
to `{DEVICE_ID}`. The same token without vendor documentation is not guessed.
Vendor-specific consent tokens such as `${GDPR_CONSENT_4594}` canonicalize to
`{GDPR_CONSENT}`. Follow-up delivery-adapter work will expand the canonical
value into the consent syntax required by its upstream platform.
Automatic translations come from a versioned macro registry keyed by source
dialect and, where available, an exact documented tracker origin. A registry
entry records the vendor, source token, canonical AdCP meaning, official
documentation, date checked, confidence, and runtime constraints. Only
verified entries are eligible for automatic translation. Unknown dialects and
generic tokens such as `{{USER_ID}}` remain unresolved until the buyer identifies
the vendor or confirms whether the value is a device advertising ID, browser
identifier, platform account ID, or something else. The mapping artifact names
the registry entry and version used so a later preview or audit can reproduce
the decision.
Use the V3 `test_creative_macros` read-only tool to inspect a tracker before it
is saved or rendered. It runs the same canonical compiler used by tracking
configuration, then uses the AdCP SDK to produce recipient syntax and an
RFC-3986-encoded synthetic substitution. Reports keep raw, canonical,
recipient, and substituted forms separate and fail closed on unresolved
required macros. The built-in scenarios cover present and unavailable device
IDs, GDPR with consent, non-GDPR traffic, and a missing required value. Supplied
test bindings are labeled `provided`; built-in values are labeled `synthetic`
or `privacy_suppressed`, and no real advertising identifier should be passed to
the tool.
Effective impression trackers are attached when a display or HTML creative
syncs to a seller and the creative has a tracker slot or HTML to carry them.
Video (VAST) creatives and other tracker types are not attached yet, and
rendered preview is separate follow-up work. Headless macro testing is
available for every tracker.
### Google Ad Manager sellers
When a seller serves through Google Ad Manager (GAM), Apostra converts each
AdCP macro in an impression tracker to GAM's own macro before sending it, and
GAM fills the value when the ad serves. Each conversion comes from Google's
[macro reference](https://support.google.com/admanager/answer/2376981).
| AdCP macro | GAM macro | Limits |
| - | - | - |
| `{CACHEBUSTER}` | `%%CACHEBUSTER%%` | None |
| `{DEVICE_ID}` | `%%ADVERTISING_IDENTIFIER_PLAIN%%` | Empty in child-directed apps and when limited ad tracking is on |
| `{DEVICE_ID_TYPE}` | `%%ADVERTISING_IDENTIFIER_TYPE%%` | As for `{DEVICE_ID}` |
| `{LIMIT_AD_TRACKING}` | `%%ADVERTISING_IDENTIFIER_IS_LAT%%` | None |
| `{GDPR}` | `${GDPR}` | None |
| `{GDPR_CONSENT}` | `${GDPR_CONSENT_}` | Empty without a valid consent string. The tracker needs its IAB vendor ID; without one, Apostra removes the consent parameter |
| `{DOMAIN}` | `%%SITE%%` | Empty when the ad request carries no page URL, and on anonymous Ad Exchange ads |
| `{PAGE_URL}` | `%%PATTERN:url%%` | Empty when the ad request carries no page URL |
| `{APP_BUNDLE}` | `%%TAG_PARAM:msid%%` | Google documents it only for video ad requests, where its video SDK sends the app ID on every platform except iOS and tvOS. Expect it empty on web pages and display ads |
| `{AXEM}` | `%%PATTERN:axem%%` | Empty on pages that do not run the Scope3 Prebid module |
Apostra fills `{MEDIA_BUY_ID}`, `{CREATIVE_ID}`, `{CAMPAIGN_ID}`, and
`{PACKAGE_ID}` itself before sync. A tracker that uses any other macro is not
sent to a GAM seller, because GAM has no equivalent. A tracker that uses
`{CLICK_URL}` is not sent either: Google counts a click every time a pixel
carrying its click macro loads, then discards those clicks as invalid. Run
`test_creative_macros` with `targetDialect: gam` to see the conversion for a
tracker before you save it.
A campaign override changes whether an inherited tracker fires; it does not
mutate the advertiser tracker. Return the original `tracker_id` when updating
a tracker so its campaign overrides and audit history remain attached. Send
the last-read `revision` as `expected_revision` for advertiser updates, or
`campaign_revision` as `expected_revision` for campaign updates, to reject a
concurrent write instead of overwriting it.
On the V3 MCP surface, this model is projected as the `tracking` field on the
existing advertiser and campaign nouns rather than as a separate tracker noun.
Use `save_advertiser({ advertiserId, tracking })` for advertiser defaults,
`save_campaign({ campaignId, tracking })` for campaign-local trackers and
inherited overrides, and `get(..., include: ["tracking"])` to read the result.
V3 uses camel-case names such as `trackerId`, `canonicalUrl`, and
`effectiveTrackers`. A tracker reads back its `url` as you saved it, beside the
translated `canonicalUrl`; custom trackers use `customEventName`. Stored free-form
values returned to a model—including tracker labels, raw and canonical URLs,
custom event names, custom-macro descriptions, and mapping-artifact strings—are
bounded and fenced as untrusted data. The V2 REST compatibility surface retains its snake-case
field names and its prior unbounded description shape. V3 accepts new or
changed custom-macro descriptions up to 240 characters; longer values stored
through V2 are bounded in model output and can be saved unchanged so legacy
configurations remain round-trip safe.
## Click-URL parameters
Click-URL parameters are query parameters Apostra appends to a creative's
click-through URL when the creative is synced to a seller. They are one system
with three layers, and every read and write surface sees the same rows:
| Layer | Set through | Wins over |
| - | - | - |
| Advertiser | v2 `utmConfig` on the advertiser, or V3 `save_advertiser` `tracking.macroAdditions` | — |
| Campaign | v2 `utmConfig` on the campaign, or V3 `save_campaign` `tracking.macroAdditions` | Advertiser, for the same key |
| Creative | V3 `save_creative` `macroAdditions` | Campaign and advertiser, for the same key |
Rules:
* **No defaults.** A new advertiser has no parameters until you set them.
Advertisers created before September 2026 carry the six `utm_*` entries
Apostra used to apply on their behalf (`utm_source=scope3`,
`utm_medium=agentic`, `utm_campaign={CAMPAIGN_ID}`, `utm_content={CREATIVE_ID}`,
`utm_media_buy={MEDIA_BUY_ID}`, `utm_package={PACKAGE_ID}`); edit or clear
them like any other entry.
* **At most 50 entries per layer.** A write that would leave a layer with more
than 50 keys is rejected; remove keys or replace the set. Advertisers
migrated in September 2026 were ported in key order up to that cap.
* **Suppression.** Through V3, a `null` value removes an inherited key at that
layer. The v2 `utmConfig` projection omits suppressed keys.
* **The authored URL wins.** A key the click-through URL already carries is
never overwritten by a parameter.
* **Macros.** A value may be a literal, or carry any
[AdCP universal macro](https://docs.adcontextprotocol.org/docs/creative/universal-macros)
as a `{MACRO}` token, for example `utm_content={CREATIVE_ID}_{DOMAIN}{APP_BUNDLE}`.
`{CAMPAIGN_ID}` and a few older Apostra names such as `{TACTIC_ID}` are also
accepted. Any other name in braces, and template syntax such as `{{...}}` or
`${...}`, is rejected.
* **Identity macros are filled when the creative syncs.** Apostra fills
`{MEDIA_BUY_ID}`, `{CREATIVE_ID}`, `{CAMPAIGN_ID}`, and `{PACKAGE_ID}` with
its own ids. A parameter whose identity macro has no value for that sync is
left off the click URL, so a seller never receives a placeholder for an id
only Apostra knows. `{PACKAGE_ID}` has a value only when the creative is
assigned to exactly one package of the media buy.
* **Other macros are filled per impression or click, not at sync.** Values such
as `{DOMAIN}`, `{APP_BUNDLE}`, `{PAGE_URL}`, and `{CACHEBUSTER}` are only
known when the ad serves. Apostra is rolling out support for keeping them in
the click URL as `{MACRO}` tokens for the seller or Apostra's click redirect
to fill. Until that is enabled for your account, a parameter that carries
one of these macros is left off the click URL when the creative syncs. Ask
your Apostra contact if you need it. The older Apostra names such as
`{TACTIC_ID}` are never filled in a click URL, so a parameter carrying one is
always left off.
* **Keys** start with a letter or digit and use URL query-key characters (max 100).
## Lifecycle
For a live advertiser, confirm the buyer operator identity, then provide
name, brand domain, required currency, sandbox mode, and optional channel
preferences. In v3, sandbox advertisers can be created before that identity
is claimed, but plan and Terms readiness still apply. Sandbox is locked
after creation.
Update channel preferences, `optimizationApplyMode`, UTM params, and
frequency caps as policy evolves. Channel overlap affects automatic seller
qualification without creating advertiser-owned selection edges.
Create campaigns under the advertiser. Discovery, audiences, and creatives
are scoped to this advertiser.
`DELETE /api/v2/buyer/advertisers/:id` archives the advertiser (soft
delete). Use `POST /api/v2/buyer/advertisers/:id/restore` to bring it back.
## Common operations
Before creating an advertiser from a domain, buyer MCP clients can call the
side-effect-free `resolve_buyer_brand` operation to show the brand-confirmation card.
The user then confirms by creating the advertiser normally.
### Create an advertiser
```bash curl theme={null}
curl -X POST https://api.apostra.com/api/v2/buyer/advertisers \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"description": "Acme global advertiser account",
"brand": "acme.com",
"primaryCurrency": "USD",
"channels": ["display", "ctv"],
"optimizationApplyMode": "MANUAL",
"sandbox": false
}'
```
```json Request body theme={null}
{
"name": "Acme Corp",
"brand": "acme.com",
"primaryCurrency": "USD",
"channels": ["display"],
"optimizationApplyMode": "MANUAL",
"linkedAccounts": [
{
"storefrontId": 1,
"sourceId": "src_main",
"accountId": "acc_67890",
"credentialId": "cred_snap_coke",
"billingType": "brand"
}
],
"frequencyCaps": [
{ "max_impressions": 3, "window": { "interval": 7, "unit": "days" } }
]
}
```
### Get / list advertisers
```bash theme={null}
# List active advertisers (summary rows)
curl "https://api.apostra.com/api/v2/buyer/advertisers?status=ACTIVE&limit=50" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
# Search by partial name or advertiser ID (search runs before pagination)
curl "https://api.apostra.com/api/v2/buyer/advertisers?status=ACTIVE&search=12345&limit=20" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
# Get a single advertiser (full resource — linkedBrand, linkedAccounts, utmConfig, frequencyCaps)
curl https://api.apostra.com/api/v2/buyer/advertisers/12345 \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
List rows return the summary shape — `id`, `name`, `status`, `sandbox`, `brand`, `linkedAccountCount`, `createdAt`, `updatedAt`. Call `GET /api/v2/buyer/advertisers/:id` for the full resource (resolved brand object, full linked-account list, UTM config, frequency caps).
```json List response theme={null}
{
"items": [
{
"id": "12345",
"name": "Acme Corp",
"status": "ACTIVE",
"sandbox": false,
"brand": "acme.com",
"linkedAccountCount": 3,
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-20T14:45:00Z"
}
],
"total": 1,
"hasMore": false,
"nextOffset": null
}
```
List supports filtering by `status`, `name` (case-insensitive partial name match), `search` (partial name or advertiser ID), and `sandbox` (true/false), plus `limit` and `offset` pagination. Filters, including `search`, are applied before pagination.
### Update an advertiser
```bash theme={null}
curl -X PUT https://api.apostra.com/api/v2/buyer/advertisers/12345 \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corporation",
"channels": ["display", "olv", "ctv"],
"optimizationApplyMode": "AUTO",
"frequencyCaps": [
{ "max_impressions": 5, "window": { "interval": 7, "unit": "days" } }
]
}'
```
Through the raw v2 API, brand domain, an already-known country scope,
currency, and reporting time zone can be re-keyed only before the first
campaign or seller-account binding. A still-unknown legacy country scope may
be classified once after first use. The canonical UI exposes that one-time
classification but not edits to known brand, country, or currency values;
create another advertiser there when known identity values must differ.
`sandbox` is always immutable. When `frequencyCaps` or `channels` is provided
on update, it **replaces** the existing set. Pass an empty array to clear;
omit the field to leave it untouched.
For a legacy advertiser that has no settings row, mixed currencies across
terminal (completed, cancelled, or archived) campaigns do not require a
replacement advertiser. When adding its brand, send `primaryCurrency` with the
intended currency from that history. Active campaigns and currently linked
seller accounts still take precedence and reject conflicting choices.
### Archive / restore
```bash theme={null}
curl -X DELETE https://api.apostra.com/api/v2/buyer/advertisers/12345 \
-H "Authorization: Bearer $SCOPE3_API_KEY"
curl -X POST https://api.apostra.com/api/v2/buyer/advertisers/12345/restore \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Archiving is a soft delete with these semantics:
* **Children stay readable.** The advertiser's campaigns, media buys, and
creatives are retained and can still be listed and read while the
advertiser is archived. Writes that would change them are still refused
until the advertiser is restored.
* **The name is released.** Name uniqueness applies to active advertisers
only, so a new advertiser may reuse an archived advertiser's name.
* **A sandbox advertiser's brand identity is released.** A new sandbox
advertiser may take an archived sandbox advertiser's brand identity (brand
domain, brand ID, countries, and currency). A non-sandbox advertiser's brand
identity stays reserved while it is archived.
* **Restore can conflict.** Restoring an advertiser returns `409 CONFLICT`
when an active advertiser now holds its name, or, for a sandbox advertiser,
its brand identity. The brand identity conflict has
`reason: brand_identity_exists` and the `advertiserId` of the active sandbox
advertiser. Only one active sandbox advertiser can hold a brand identity, so
archive that advertiser (or rename the other, for a name conflict) before
restoring.
* **Restore reopens ledger accounts.** The advertiser's archived ledger
accounts are reopened as part of the restore.
From the v3 agent surface, `save_advertiser` performs the same operations with
`isArchived: true` / `false`, and accepts `advertiserIds` to archive or
restore several advertisers in one call.
## Linked accounts
The `linkedAccounts` field connects the advertiser to existing accounts on upstream platforms — Snap, programmatic DSPs, agency seats. Each linked account is the pre-existing seat ID a partner already has for this brand or buyer; the link makes that ID addressable to Apostra for cross-platform reporting and pass-through execution.
Use `linkedAccounts` whenever the buyer **already has** a relationship with
the upstream platform — an existing Snap seat, a DV360/Trade Desk account, an
agency-managed seat. Linked accounts let Apostra reuse the seat ID for
unified reporting and pass-through execution without re-provisioning
credentials per call. This account choice is independent of campaign mode,
BYOA, protocol connectivity, and settlement; do not infer those from
`routingType`.
### Fields
Each link maps these fields:
| Field | Type | Notes |
| - | - | - |
| `storefrontId` | integer | Storefront the source lives on. Get from `GET /api/v2/buyer/storefronts`. |
| `sourceId` | string | Inventory source within `storefrontId` whose account is being linked. |
| `accountId` | string | The seller-issued AdCP/source account ID (e.g. `acc_67890`). Must come from account discovery — Apostra does not mint it and clients must not substitute the internal `linkId`. |
| `credentialId` | string \| null | The connected provider credential that owns this linked account. Include it when creating or updating links if multiple credentials can expose the same `accountId`. |
| `billingType` | string (optional) | The billing arrangement for this link. Defaults to the source's default billing type when omitted. |
`billingType` is a source-defined string. The supported values per source are reported on the discovery endpoint (`GET /api/v2/buyer/advertisers/:advertiserId/accounts/available?storefrontId=...&sourceId=...` returns `billingOptions.supported` and `billingOptions.default`). Common values:
| Value | Meaning |
| - | - |
| `brand` | The advertiser is billed directly by the partner |
| `agency` | An agency is the billing entity acting on behalf of the brand |
Other values may be supported by individual partners — always check `billingOptions.supported` before sending.
### Endpoints
| Method | Path | Purpose |
| - | - | - |
| `GET` | `/api/v2/buyer/advertisers/:advertiserId/accounts` | List linked accounts on the advertiser |
| `GET` | `/api/v2/buyer/advertisers/:advertiserId/accounts/available` | List accounts the source has discovered but not yet linked |
| `GET` | `/api/v2/buyer/advertisers/:advertiserId/accounts/:accountId` | Get a single linked account |
| `POST` | `/api/v2/buyer/advertisers/:advertiserId/accounts` | Link a source account to the advertiser |
### List linked accounts
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/advertisers/12345/accounts?storefrontId=1&sourceId=src_main&status=active&take=50" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Filters: `storefrontId` + `sourceId` (must be paired), `status` (`active`, `pending_approval`, `payment_required`, `suspended`, `closed`). Pagination: `take`, `skip`.
```json Response theme={null}
{
"data": {
"accounts": [
{
"linkId": "42",
"accountId": "acc_67890",
"credentialId": "cred_snap_coke",
"name": "Acme c/o Pinnacle",
"sources": [
{
"storefrontId": 1,
"storefrontName": "Retail Media Network",
"sourceId": "src_main",
"sourceName": "Retail Network Agent"
}
],
"advertiserId": "12345",
"billing": "brand",
"status": "active",
"createdAt": "2025-01-15T10:30:00Z",
"updatedAt": "2025-01-20T14:45:00Z"
}
],
"total": 1
}
}
```
### Link an account
```bash theme={null}
curl -X POST https://api.apostra.com/api/v2/buyer/advertisers/12345/accounts \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"storefrontId": 1,
"sourceId": "src_main",
"accountId": "acc_67890",
"billingType": "brand"
}'
```
The response carries the full `AccountOutput` with the server-assigned `linkId`. To pre-fill the request body, call `GET /accounts/available?storefrontId=1&sourceId=src_main` first — it returns every discovered account with its supported `billingOptions`.
### Get a single linked account
```bash theme={null}
curl https://api.apostra.com/api/v2/buyer/advertisers/12345/accounts/acc_67890 \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Returns the same `AccountOutput` shape as list. Use this to confirm a link's current `status` after a source-side state change (e.g. moving from `pending_approval` to `active`).
Linked accounts surface external account IDs (e.g. `acc_67890`) so
cross-platform reporting and pass-through execution can target them without
re-discovering credentials. The `linkId` is Apostra-side primary key
for the link itself; the `accountId` is the source-side account.
## Sandbox advertisers
Set `sandbox: true` at creation to mark the advertiser as test-only. All ADCP operations under sandbox advertisers — discovery, media buy creation, execution — route to sandbox-flagged sales agent accounts. No real platform calls are made and no real spend occurs. In the v3 `save_advertiser` journey, you can create, update, and archive a sandbox advertiser before claiming the buyer operator identity. Plan and Terms readiness checks still apply; this exemption is only for the operator-identity check.
Use sandbox advertisers to:
* Rehearse the discovery → execute → report flow end-to-end
* Test creative manifest workflows without notifying publishers
* Train AI agents against the API without billing implications
## Optimization apply mode
`optimizationApplyMode` controls whether Apostra's RL optimizer applies suggestions to media buys automatically.
* `MANUAL` (default) — suggestions appear in the suggestion feed; a human or agent must approve before they hit media buys
* `AUTO` — suggestions are applied immediately
The advertiser-level setting is the default for campaigns under the advertiser. Each campaign can override it.
## Related concepts
Marketing initiatives owned by the advertiser
Brand identity resolved from AdCP registry or `/.well-known/brand.json`
Buyer-side caps enforced by Apostra across publishers
Manifest-based creatives nested under campaigns
# Brand
Source: https://docs.apostra.com/v2/object-guides/brand-story
Brand identity resolved from the AdCP brand registry — logos, colors, tone, product catalog, and disclaimers
## Overview
A **Brand** in v2 is the resolved identity behind an [Advertiser](./advertiser): the canonical name, logos, colors, fonts, tone, tagline, assets, product catalog, disclaimers, contact info, and target audience. Brand identity is **not stored on the advertiser directly**. Instead, the advertiser references a brand domain (e.g. `northwind.example`) and Apostra resolves the full manifest from the brand's own manifest, the registry, or enrichment:
1. The brand's own `/.well-known/brand.json`
2. The **Agentic Advertising Organization (AAO)** brand registry maintained by Apostra and partners
3. Brandfetch enrichment when no official manifest or registry entry is available
This decouples brand identity from any single advertiser account — the same brand can power multiple advertisers across customers, agencies, and storefronts, and a brand update propagates everywhere it's referenced.
A published or registry-hosted manifest is optional for channel-specific app
branding. When no reusable logo resolves, configure that channel's required
assets directly instead.
**Where this differs from v1**: V1 had a separate "Brand Stories" object describing target audiences in natural language. V2 resolves brand identity from `brand.json`, the registry, or enrichment. Audience descriptions now belong on signals, brand stories where supported, and campaign briefs. This guide covers the v2 brand resolution model.
## Why brands matter
* **Single source of truth** — one canonical manifest per brand domain, used by every advertiser referencing it
* **Auto-population** — creatives auto-resolve brand\_domain from the campaign's advertiser, so logos/colors/fonts flow into manifests without manual entry
* **Storefront onboarding** — discovery and storefront sign-in flows resolve a brand from the browser's domain so users land in the right advertiser context
* **AAO interoperability** — agents across the AdCP ecosystem can negotiate from the same resolved brand identity
## Key fields (BrandManifestJson)
The full manifest follows ADCP v2. Top-level fields:
| Field | Type | Notes |
| - | - | - |
| `name` | string | Brand name (required) |
| `url` | string | Brand website URL |
| `logos` | array | `{ url, tags?, width?, height? }` — primary, dark, square variants |
| `colors` | object | `{ primary, secondary, accent, background, text }` hex codes (extra keys preserved) |
| `fonts` | object/array | Primary/secondary fonts; supports both v2 object form and Brandfetch array form |
| `tone` | string | Voice and tone description |
| `tagline` | string | Brand tagline / slogan |
| `assets` | array | Brand assets (`assetId`, `assetType`, `url`, dimensions, metadata) |
| `productCatalog` | object | `{ feedUrl, feedFormat, categories, lastUpdated, updateFrequency }` |
| `disclaimers` | array | `[{ text, context?, required }]` legal disclaimers |
| `industry` | string | Sector classification |
| `targetAudience` | string | Target audience description |
| `contact` | object | `{ email, phone, website }` |
| `metadata` | object | `{ createdDate, updatedDate, version }` |
## Resolution flow
When you create or update an advertiser with `brand: "acme.com"`:
Apostra fetches `https://acme.com/.well-known/brand.json`. If present and valid, it's used as the manifest.
If the well-known location is missing or invalid, Apostra looks up `acme.com` in the AAO brand registry.
Pass `saveBrand: true` after reviewing enrichment to persist brand identity into the registry when the brand isn't yet registered. When no enrichment data exists, use `saveBrand: true` only after confirming the advertiser name and brand domain.
The advertiser response carries a `linkedBrand` object containing the resolved name, domain, logos, colors, industry, tone, tagline, and the full manifest.
If the manifest can't be resolved, the advertiser response includes `brandWarning` describing why — but the advertiser is still created so onboarding can continue.
## Know who manages the identity
Resolution and editing are separate concerns:
* **Self-hosted** — Apostra reads `/.well-known/brand.json`, but the domain
owner edits and republishes it on their own website.
* **AAO-managed** — connect the corresponding AAO account to manage the hosted
identity there.
* **Community profile** — claim the profile through AAO before managing it.
* **Missing** — use the AAO builder to create a file for self-hosting or request
AAO-managed hosting. You can also skip this optional step when the consuming
channel supports direct asset uploads.
## ADCP v1 compatibility
For backwards compatibility with the AdCP v1 wire format, Apostra also accepts and resolves manifests using:
* **House portfolio** — house-of-brands hierarchies with master/sub\_brand/endorsed/independent relationships
* **Authoritative location redirect** — `{ "authoritative_location": "https://other-domain/.well-known/brand.json" }`
* **House redirect** — `{ "house": "parent-domain.com" }` to resolve from a parent house
* **Brand agent delegation** — `{ "brand_agent": { "url": "...", "id": "..." } }` to delegate to an MCP endpoint
The platform extracts the canonical brand name from any of these forms.
House-portfolio manifests may also include `authorized_operators`. Apostra can use that field as trusted evidence for storefront operator-domain auto-verification when the registered account domain is already approved and the manifest links the account domain to the requested alias or rebrand operator domain. The `operator_domain` check on your storefront readiness reports what is still outstanding and who owns the next step.
## Common operations
Brand resolution happens implicitly through the advertiser endpoints — there is no standalone `POST /v2/brands` for buyers. The relevant moments:
### Create an advertiser with a brand domain
```bash theme={null}
curl -X POST https://api.apostra.com/api/v2/buyer/advertisers \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme",
"brand": "acme.com",
"saveBrand": false
}'
```
If `acme.com` resolves successfully, the response carries `linkedBrand` with the manifest. If not, `brandWarning` explains.
### Save an enriched brand to the registry
If an advertiser response includes `brandWarning` because the brand resolved through enrichment rather than the AAO registry, review the returned brand details first. To persist the enriched brand, update the existing advertiser with `saveBrand: true`:
```bash theme={null}
curl -X PUT https://api.apostra.com/api/v2/buyer/advertisers/12345 \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand": "acme.com",
"saveBrand": true
}'
```
This persists the enriched identity so future references resolve without re-enrichment. If registry/enrichment lookup has no brand data, retry advertiser creation with `saveBrand: true` only after confirming the advertiser name and brand domain.
### Re-resolve a brand on an existing advertiser
```bash theme={null}
curl -X PUT https://api.apostra.com/api/v2/buyer/advertisers/12345 \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "brand": "acme.com" }'
```
Updating the `brand` field re-fetches the manifest. Use this when you've published a new `/.well-known/brand.json` or the AAO registry entry has changed.
### Inspect resolved brand
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/advertisers/12345?includeBrand=true" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
The response includes `linkedBrand` with the canonical fields plus the full `manifest`.
## How creatives use the brand
When you create a creative manifest under a campaign, `brand_domain` auto-populates from the campaign's advertiser. Creative AI generators consume the resolved manifest's logos, colors, fonts, tone, tagline, disclaimers, and product catalog so generated assets stay on-brand without per-creative configuration.
If your manifest defines `disclaimers` flagged `required: true`, downstream creative pipelines surface them for inclusion.
## Storefront onboarding
The storefront sign-in flow uses brand resolution to map a visitor's browser domain (or AAO sign-in) to the correct advertiser. When a buyer authenticates through a storefront:
1. The storefront resolves the active brand domain
2. Apostra looks up advertisers linked to that brand
3. The buyer is dropped into the right advertiser context — private/member agents become discoverable
See the storefront onboarding setup guide for the full flow.
## Best practices
Publish `/.well-known/brand.json` on your brand's primary domain. This is the authoritative source — registry entries are a fallback. Keep it in sync with downstream brand systems.
Use the AdCP v1 house-portfolio form to model house-of-brands hierarchies (one master brand, multiple sub-brands). Each sub-brand can resolve through the parent house manifest.
Treat `disclaimers` as the single source of truth for legal text. Mark each `required: true` and supply `context` so creative pipelines know when to apply them.
Point `productCatalog.feedUrl` at a Google Merchant Center, Facebook Catalog, or custom feed. AdCP-aligned discovery and DCO can reference catalog SKUs directly.
## Related concepts
Owns the `brand` reference; surfaces resolved brand under `linkedBrand`
Auto-resolves brand\_domain from the campaign's advertiser
Audience definitions complementary to brand identity
Brand-aware sign-in surfaces the right advertiser context
# Campaign
Source: https://docs.apostra.com/v2/object-guides/campaign
The unit of media planning — flight dates, budget, products, audiences, creatives, and pacing
## Overview
A **Campaign** is a single coherent media plan owned by an [Advertiser](/v2/object-guides/advertiser). The read-only `mode` field projects the campaign's configuration as `discovery`, `performance`, or `directed`. Managed campaigns define flight dates, budget, optimization goals, audiences, creative requirements, and (after execution) the resulting **media buys** with publishers. A tracked campaign is instead a read-only projection of one storefront-managed media buy. Its `directed` mode is a deprecated compatibility value; use `management: "tracked"` to identify the projection.
In v2, the campaign is the focal object. For platform-managed campaigns,
discovery sessions, products, creatives, audiences, frequency caps, and pacing
schedules attach to it, and execution turns selected products into one or more
media buys per channel group, sales agent, and settlement currency. Tracked campaigns do not execute:
subscription projects one existing upstream campaign into one campaign shell
and one media buy.
An external AdCP client calling an Apostra-hosted storefront creates a directed
buyer campaign when the buyer passes that storefront's commercial checks: the
task addresses one storefront and the storefront manages execution. Apostra
anchors the contract, governance, durable journal, and dual-key media-buy
identity before dispatch, then records accepted commitments and delivery on its
ledger. Public Listing + Distribution controls discovery, not campaign-buying
eligibility. The commercial envelope does not add a cross-storefront management
layer. See
[How Apostra uses AdCP](/v2/concepts/adcp-on-the-buy-side#external-buyer-to-apostra-to-seller-alpha).
## Campaign history and compounding intelligence
Every campaign contributes to its advertiser's history from the moment it is
created. The durable record includes the brief, selected products and media
buys, decisions made through Interchange, and the delivery or performance
results received for the campaign. An authorized agent can use that
advertiser-scoped history when planning later campaigns.
A campaign does not need conversion or other external measurement data to add
to this history. Its brief and decisions still remain available. Reporting and
measurement results join the same record as they arrive; Interchange does not
invent outcome metrics before a source reports them.
**`routingType` is legacy billing compatibility metadata, not a campaign or
storefront model.** It is a lossy observation derived from BillingParty for old
rate-card and ledger consumers. You do not set it. Campaign mode,
bring-your-own buyer agent, protocol endpoint, execution path, optimization,
credentials, targeting, and settlement are independent concerns and must not be inferred
from this field.
## Choosing a composition path
**Composition ownership** — who picks the inventory — is controlled by
`autonomy.inventorySelection` (`manual` | `propose` | `automatic`), not by
optimization goals. A campaign with optimization goals (`performanceConfig`) can
still be manually composed.
* `manual`: the buyer picks sellers and products directly. This is the default.
* `propose`: the agent drafts a selection and waits for approval.
* `automatic`: the platform optimizer (`auto_select_products`) owns product
selection. Manual `create_media_buys` calls are rejected on these campaigns.
The read-only `mode` field reflects the campaign's configuration as a v2
read projection: `discovery` (no goals, no tracked buy), `performance`
(optimization goals present), or `directed` (tracked buy). Mode is not a
write-side gate. Setting optimization goals on an existing campaign is allowed
regardless of its current mode projection. After the backfill (AI-8988),
`mode: 'performance'` means "this campaign has goals"; it no longer means
"the platform composes this campaign" — composition is controlled by
`autonomy.inventorySelection`.
Naming one storefront or ad platform does not determine composition path. If the
request is “buy TikTok” or “buy Meta,” first clarify which system should remain
the campaign source of truth:
* A tracked projection returns `mode: “directed”` when the provider remains
authoritative for a single one-to-one media buy. It is tracked and read-only;
create the upstream campaign in the connected provider account.
* Use `autonomy.inventorySelection: “manual”` (the default) when the buyer
explicitly selects products. A discovery search may be scoped to one named
storefront. After selection and confirmation, launch it with
`execute_campaign`.
* Use `autonomy.inventorySelection: “automatic”` when Apostra should
allocate across sellers toward an objective. Set `performanceConfig` to
provide optimization goals for the optimizer.
To retire an existing performance configuration, send `performanceConfig: null`.
A goal with a target is also what sellers price against and what delivery is
judged on. [Goal-seeking campaigns](/v2/concepts/goal-seeking-campaigns)
explains the four parts of a goal statement, who counts each kind of goal, and
what a seller can and cannot promise for it.
### Optimization goal target strength (`performanceConfig`)
A `performanceConfig.optimizationGoals[].target` of kind `cost_per` or `per_ad_spend` can optionally set `strength` to say how firmly the seller should hold it. `target` aims for the value without a hard guarantee. The other allowed value is target-kind-specific: a `cost_per` target may also set `strength: "cap"` (a hard maximum cost); a `per_ad_spend` target may also set `strength: "floor"` (a hard minimum return). When you omit `strength`, a seller on AdCP 3.2 that accepts a package bidding policy receives a cost target as `cap` and a return target as `target`; a seller that reads the target on the goal applies AdCP's default, `target` (see [how a package carries a cost or return target](/v2/concepts/goal-seeking-campaigns#how-a-package-carries-a-cost-or-return-target)). `cap` is invalid on a `per_ad_spend` target and `floor` is invalid on a `cost_per` target — both are rejected as validation errors, not silently ignored. Not every seller supports every strength — **Meta only supports `cap` for `cost_per` and `floor` for `per_ad_spend`, and rejects the default `target` strength for both.** Set it explicitly (e.g. `{ "kind": "per_ad_spend", "value": 3.0, "strength": "floor" }`) when targeting a seller with this constraint.
## Goal configuration in V3
In V3, `save_campaign.optimizationGoals` is opt-in. Send an array only when
the buyer explicitly states the goal; a metric word in a campaign name or brief
is a question to ask, not a goal to set. On an update, omit
`optimizationGoals` to keep the current goals, or send `optimizationGoals:
null` to clear them and return the campaign to discovery.
V3 `get` of a campaign returns its `optimizationGoals` in the same shape
`save_campaign` takes, primary goal first, as they were saved. A cost or
return target is read as `bidding`, never on a goal, so a campaign read and
sent back unchanged saves without changing anything. The field is absent when
the campaign has no goals. To see whether a goal's event source reaches each
seller account, read the event source: `get({ kind: "event_source" })` returns
`sellerAccounts[]` with each account's `status`, `blocker`, and the seller's
`health` for its pixel.
Each V3 goal is an AdCP `OptimizationGoal` in AdCP's snake\_case. An `event`
goal takes `event_sources[]` (`event_source_id`, `event_type`, and optionally
`custom_event_name`, `value_field`, `value_factor`), an optional
`attribution_window` (`post_click`, optional `post_view`), an optional
`target`, and `priority`. A `metric` goal takes `metric`, optional
`view_duration_seconds`, an optional `target`, and `priority`; AdCP defines no
attribution window for a metric goal, so V3 refuses one. Cost and return
targets go in `bidding`, not on a goal.
```json theme={null}
{
"optimizationGoals": [
{
"kind": "event",
"event_sources": [
{ "event_source_id": "website_pixel", "event_type": "purchase", "value_field": "order_total" }
],
"attribution_window": { "post_click": { "interval": 7, "unit": "days" } },
"priority": 1
},
{ "kind": "metric", "metric": "completed_views", "view_duration_seconds": 15, "priority": 2 }
]
}
```
[Campaigns on connected provider accounts](/v2/buyer/campaigns/directed-campaigns)
documents the read-only subscribe, mirror, refresh, and delivery lifecycle.
## Key fields
`labels` is the Buyer Account's optional dimension-keyed classification for the
campaign. It is managed through the V3 MCP `save_campaign` tool and read
through V3 `get` and `search`; see [Dimensions and labels](/v2/object-guides/dimension).
| Field | Type | Notes |
| - | - | - |
| `campaignId` | string | Stable campaign identifier |
| `mode` | enum | Read-only projection: `discovery` (no goals), `performance` (goals present), or `directed` (tracked buy). The `directed` value is a deprecated compatibility alias — read `management` for the platform's behaviour. Set `autonomy.inventorySelection` to control composition ownership, not `mode`. |
| `management` | enum | `tracked` (mirrored from a connected provider account the platform did not set up; read-only shell) or `managed` (uses the canonical Apostra buyer lifecycle) |
| `advertiserId` | string | Owning advertiser |
| `catalogId` | string | Optional advertiser catalog bound through V3 `save_campaign`; `null` clears it. It reads back on the campaign, and `catalogArchived` marks a bound catalog that was archived after binding. A bound catalog blocks launch until seller catalog sync ships in AI-10203. |
| `name` | string | Campaign name (max 255) |
| `status` | enum | `DRAFT`, `ACTIVE`, `PAUSED`, `COMPLETED`, `CANCELED`, `ARCHIVED` |
| `flightDates` | object | `{ startDate, endDate }` ISO dates |
| `budget` | object | `{ total, currency, dailyCap?, pacing? }`. Gross — fee-inclusive, like every buyer budget. See [Budgets and fees](/v2/concepts/budgets-and-fees). `pacing` (`EVEN`, `ASAP`, `FRONTLOADED`) is sent to the seller on every package whose product line sets none; see [Which pacing a package gets](/v2/buyer/campaigns/packages#which-pacing-a-package-gets) |
| `targetingOverlay` | object | Optional campaign-level AdCP TargetingOverlay. Omit it to leave it unchanged; `null` clears V3-managed fields. Regions use ISO 3166-2 codes. Audience inclusion/exclusion remains in `audienceConfig`. |
| `allocatedBudget` | number | Sum of the budgets of every media buy that still holds budget — `DRAFT`, `PENDING_APPROVAL`, `INPUT_REQUIRED`, `ACTIVE`, `PAUSED` — plus delivered spend (all-in, at each buy's locked fee terms) on ended or archived media buys. Gross, the same denomination as `budget.total`. A `DRAFT` buy is already counted |
| `unallocatedBudget` | number | `budget.total - allocatedBudget` — room left to allocate, whether to a new media buy or as an increase to an existing one. Can go negative when delivered spend on ended buys exceeds `budget.total` |
| `pacingPeriods` | object | Optional time-windowed pacing schedule with weight or budget mode |
| `constraints` | object | Channel filter plus AdCP-shaped targeting overlay (geo, language, device). Targeting fields flow into every media-buy package: include lists intersect with package-level targeting, exclude lists union. |
| `channelGroups` | array | Optional buyer-declared inventory selections. Each group combines AdCP channel, property type, device type, and format dimensions and creates a separate media-buy/reporting boundary. See [Channel groups](/v2/buyer/campaigns/channel-groups). |
| `performanceConfig` | object | Optimization goals (event-based or metric-based) |
| `optimizationApplyMode` | `AUTO` \| `MANUAL` | Inherits advertiser default if unset |
| `audiences` | array | Target and suppress audiences |
| `mediaBuys` | array | Spawned at execution for platform-managed campaigns; exactly one mirrored upstream buy for a directed campaign |
| `creativeFormats` | object | `{ required, covered, missing }` — surfaces formats still needing creatives |
| `discoveryId` | string | Discovery session feeding product selection (DRAFT only) |
| `productCount` | number | Selected products (DRAFT only — after execute, products are inside media buys) |
| `frequencyCaps` | array | Buyer-side caps for this campaign |
| `utmConfig` | array | **Read shape.** Resolved click-URL parameters, campaign entries over advertiser entries, each `{ paramKey, paramValue, source }`. Present only when at least one resolves. The write shape on create/update is an object; see [Click-URL parameters](#click-url-parameters) |
## Tracker inheritance
Campaign tracking configuration is part of the campaign rather than a separate
top-level resource. Read the resolved configuration with
`GET /api/v2/buyer/campaigns/:campaignId/tracking-config` and replace the
campaign-local portion with `PUT` on the same path.
Each campaign resolves the advertiser's current tracker defaults, then applies
its own `custom_tracker_urls` and `tracker_overrides`. The response exposes the
deterministic `effective_trackers` list, including each tracker's
`source_scope`, inherited state, effective enabled state, and status reason.
It also returns `advertiser_revision`, `campaign_revision`, and an
`effective_revision` spanning both inputs. Advertiser trackers are referenced
by stable `tracker_id`; they are not copied into every campaign.
To avoid overwriting a concurrent edit, send the last-read
`campaign_revision` as `expected_revision` on campaign updates. A stale value
returns `409 CONFLICT`. Disabling an inherited tracker creates a campaign
override and does not modify the advertiser default. If that advertiser
tracker is later removed, its override remains visible in
`orphaned_override_ids` rather than attaching to another tracker.
Tracker imports preserve both the vendor's exact `raw_url` and the canonical
AdCP-macro `url`. See [Tracker defaults and campaign inheritance](/v2/object-guides/advertiser#tracker-defaults-and-campaign-inheritance)
for macro mapping, publishability, and V3 `tracking` field behavior.
## Platform-managed lifecycle
The steps below apply to managed `discovery` and `performance` campaigns. A
tracked campaign has no DRAFT planning or execute step. Its
mirror can be refreshed read-only. See
[Campaigns on connected provider accounts](/v2/buyer/campaigns/directed-campaigns)
for its connect → map → subscribe → mirror lifecycle.
An inbound external-AdCP campaign is also platform-managed, but the external
client performs the AdCP media-buy lifecycle directly; it does not separately
call the buyer API's `execute_campaign`. Its alpha create is anchored before
seller dispatch. Supported update and cancellation operations use the same
governed campaign spine.
Create the campaign with `flightDates`, `budget`, and optional `brief`/`constraints`/`channelGroups`. Attach a discovery session, select products, attach audiences, configure pacing periods. When channel groups are defined, assign every selected product to one group.
Use `GET /api/v2/buyer/campaigns/:campaignId/creatives/templates` to see required formats, then upload manifest-based creatives via `POST /api/v2/buyer/campaigns/:campaignId/creatives/create` (multipart). Listing manifests is `GET /api/v2/buyer/campaigns/:campaignId/creativeManifest`. Inspect `creativeFormats.missing` to confirm coverage.
`POST /api/v2/buyer/campaigns/:id/execute` launches a `DRAFT` (or `COMPLETED`) campaign — it groups selected products by channel group, sales agent, and settlement currency, packages them per pacing period, and submits the resulting media buys to AdCP. The campaign transitions DRAFT → ACTIVE. If submission fails and a media buy remains `DRAFT`, retrying execute submits that same buy without rebuilding it from discovery, so corrected per-buy settings remain intact.
`POST /api/v2/buyer/campaigns/:id/pause` halts spend across all media buys. To resume a `PAUSED` campaign, call `POST /api/v2/buyer/campaigns/:id/reactivate` — this is a separate endpoint from `execute` and is the only way to bring a paused campaign back to `ACTIVE`.
Reaching `flightDates.endDate` or full delivery transitions the campaign to COMPLETED.
## Common operations
### Create a campaign
This operation creates a managed campaign. To track an upstream campaign,
create it in the connected provider account and subscribe to that account's read-only
mirror.
```bash curl theme={null}
curl -X POST https://api.apostra.com/api/v2/buyer/campaigns \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"advertiserId": 12345,
"name": "Q2 2026 — Tech Launch",
"flightDates": { "startDate": "2026-05-15T00:00:00Z", "endDate": "2026-07-15T23:59:59Z" },
"budget": { "total": 100000, "currency": "USD", "pacing": "EVEN" },
"brief": "Premium video for tech-savvy professionals",
"constraints": {
"channels": ["ctv", "video"],
"geo_countries": ["US", "CA"],
"geo_metros": [{ "system": "nielsen_dma", "values": ["501", "803"] }],
"language": ["en"]
}
}'
```
```json With pacing periods theme={null}
{
"advertiserId": 12345,
"name": "Holiday Heavy-Up",
"flightDates": { "startDate": "2026-11-01T00:00:00Z", "endDate": "2026-12-31T23:59:59Z" },
"budget": { "total": 250000, "currency": "USD" },
"pacingPeriods": {
"mode": "weight",
"periods": [
{ "label": "Pre-Black Friday", "start": "2026-11-01T00:00:00Z", "end": "2026-11-26T23:59:59Z", "weight": 1.0 },
{ "label": "Black Friday / Cyber Monday", "start": "2026-11-27T00:00:00Z", "end": "2026-12-02T23:59:59Z", "weight": 3.0 },
{ "label": "Holiday push", "start": "2026-12-03T00:00:00Z", "end": "2026-12-31T23:59:59Z", "weight": 1.5 }
]
}
}
```
### List / get campaigns
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/campaigns?advertiserId=12345&status=ACTIVE" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
curl https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321 \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Add `mode=discovery`, `mode=performance`, or `mode=directed` to filter the
shared campaign list without splitting reporting into separate resources.
The list defaults to the live working set — every non-terminal status
(`ACTIVE`, `DRAFT`, `PAUSED`) across both management states, so anything
that could still spend shows up by default while terminal history does not.
Pass explicit statuses or `status=ALL` to reach completed, canceled, or
archived campaigns; add
`management=managed` or `management=tracked` to narrow by who operates the
campaign. Tracked scale belongs to the provider-account relationship
rollup, not the ambient list: a subscribed account may mirror thousands of
historical campaigns.
`LIST` returns each campaign in a compact summary shape (identity, mode, status,
flight dates, flattened budget, and product count). Directed summaries also
carry provider and mirror freshness. Use `GET /campaigns/{id}` for the full
mode-specific resource. Platform-managed details include brief, audiences,
creative format coverage, budget allocation, pacing, performance config, and
frequency caps; directed details project the single upstream media buy and its
packages.
### Auto-select products
With a discovery session, let Apostra pick a balanced product set from eligible inventory:
```bash theme={null}
curl -X POST https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/auto-select-products \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "maxProducts": 8, "minBudgetPerProduct": 2500 }'
```
Iterate with ADCP-style refinement:
```json theme={null}
{
"refine": [
{ "scope": "request", "ask": "more video, less display" },
{ "scope": "product", "id": "prod_xyz789", "action": "more_like_this" },
{ "scope": "product", "id": "prod_xyz790", "action": "omit" }
]
}
```
### Execute / pause / reactivate
```bash theme={null}
# Launch a DRAFT (or COMPLETED) campaign — turns selected products into media buys and submits to ADCP
curl -X POST https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/execute \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "debug": false }'
# Pause every media buy on the campaign
curl -X POST https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/pause \
-H "Authorization: Bearer $SCOPE3_API_KEY"
# Resume a PAUSED campaign
curl -X POST https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/reactivate \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
The execute response reports `success`, `previousStatus`, `newStatus`, and structured `errors[]` when individual media buy submissions fail. Pass `debug: true` to surface the raw ADCP request/response payloads in error details.
`execute` and `reactivate` are distinct endpoints. `execute` launches a `DRAFT` or `COMPLETED` campaign for the first time (or relaunches it). `reactivate` is the only way to bring a `PAUSED` campaign back to `ACTIVE`.
### Update a running campaign
`PUT /api/v2/buyer/campaigns/:id` accepts partial updates. The `mediaBuys[]` array on update lets you act on individual media buys in one call:
```json theme={null}
{
"mediaBuys": [
{
"action": "update",
"mediaBuyId": "mb_abc123",
"packages": [{ "packageId": "pkg_1", "budget": 15000, "pacing": "asap" }],
"optimizationGoals": [
{ "kind": "metric", "metric": "completed_views", "target": { "kind": "cost_per", "value": 0.08 } }
]
},
{ "action": "cancel", "mediaBuyId": "mb_def456", "reason": "underperforming" }
]
}
```
Always confirm `optimizationGoals` with the buyer before changing them — silent goal changes break optimization continuity.
When the update payload includes `pacingPeriods`, the response also contains a `pacingCascadeResult` block summarizing the per-media-buy outcome of pushing appended periods to live media buys. See the [Pacing Periods guide](/v2/guides/pacing-periods) for the response shape, append-only rules, and unsupported-agent fallback.
### Live ADCP status
```bash theme={null}
curl https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/media-buy-status \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
This polls the sales agents directly and persists any status changes. Useful right after execution while waiting on publisher approvals.
## Media buys and packages
Media buys can be staged directly as `DRAFT` resources before campaign execution through the v3 `save_media_buy` tool. Campaign execution also reconciles the campaign cart and submits its drafts. Each media buy represents one AdCP transaction with one sales agent, one settlement currency, and, when configured, one channel group. You read their status and tune them via the campaign update endpoint.
**Hierarchy**: Campaign → Media Buy (one per channel group × sales agent × settlement currency) → Package (one per product per pacing period) → Delivery.
### Media buy status
`status` is one of `DRAFT`, `PENDING_APPROVAL`, `INPUT_REQUIRED`, `ACTIVE`, `PAUSED`, `COMPLETED`, `CANCELED`, `FAILED`, `REJECTED`, `ARCHIVED`. For multi-agent campaigns, the surfaced status is the **most restrictive** across agents — e.g. one `ACTIVE` and one `PENDING_APPROVAL` reports as `PENDING_APPROVAL`. `INPUT_REQUIRED` indicates a seller needs additional information before approval can proceed.
Status updates flow via:
* **Webhooks** from sales agents (preferred — near-real-time)
* **Polling** via `GET /api/v2/buyer/campaigns/:campaignId/media-buy-status`, which queries each agent and persists changes
### Optimization goals at the media-buy level
Goals are applied to every package in a media buy at execution time. Each goal is either event-based or metric-based:
```json Event goal — optimize for tracked conversions theme={null}
{
"kind": "event",
"event_sources": [
{ "event_source_id": "website_pixel", "event_type": "purchase", "value_field": "value" }
],
"target": { "kind": "cost_per", "value": 25.0 }
}
```
```json Metric goal — optimize for seller-native delivery metrics theme={null}
{
"kind": "metric",
"metric": "completed_views",
"target": { "kind": "cost_per", "value": 0.08 }
}
```
| Target kind | Meaning |
| - | - |
| `cost_per` | Target CPA (events) or CPM-like rate (metrics) |
| `per_ad_spend` | Target ROAS — value per dollar spent (events only) |
| `maximize_value` | No target — maximize total event value within budget |
| `threshold_rate` | Maintain a minimum rate of the metric (metrics only) |
Media-buy-level goals do not yet support an explicit `strength` on a `cost_per`/`per_ad_spend` target — some sellers (e.g. Meta) require a specific strength (`cap` for `cost_per`, `floor` for `per_ad_spend`) and reject a media-buy-level goal that omits it. Set the goal on `performanceConfig.optimizationGoals` instead (see the field table above) when targeting such a seller — it does support `strength`.
Supported metrics: `clicks`, `views`, `completed_views`, `viewed_seconds`, `attention_seconds`, `attention_score`, `engagements`, `follows`, `saves`, `profile_visits`, `reach`. For a reach or CPM-style goal, target `metric: "reach"` with a required `reach_unit` (`individuals`, `households`, `devices`, `accounts`, `cookies`, or `custom`) and an optional `target_frequency` — see [Optimization goals](/v2/buyer/campaigns/media-buys#optimization-goals) for the full shape.
#### Meta Instant Form conversion leads
When product discovery on a connected Meta account returns **Meta Leads —
Instant Form Conversion Leads**, you can optimize for people who are more
likely to become qualified leads. This matches Meta Ads Manager's **Maximize
number of conversion leads** performance goal.
Select an authorized Facebook Page and one of its active Instant Forms. The
product binds the event source `meta:instant_forms` and event type
`qualify_lead`; a messaging, website, or other event source is not a compatible
substitute. Create and update requests fail before changing the Meta campaign
if the selected Page or exact Form is no longer eligible.
Delivery reporting uses Meta's `conversion_leads` result as
`ext.meta.qualified_leads`. It is authoritative for this provider-qualified
signal, including an explicitly reported zero; it does **not** populate the
generic `leads` or `conversions` fields. Connecting CRM feedback through Meta's
Conversions API is recommended because it gives Meta more information about
which leads become qualified, but Meta does not enable that integration
automatically and it is not required to create the media buy.
Existing campaign setup and optimization behavior, and the standard Instant
Form leads product, are unchanged.
### Packages
When a media buy executes, products turn into **packages** — one per product, multiplied by the number of pacing periods. Each package carries its own budget, pacing strategy (`even`, `asap`, `front_loaded`), bid price, flight start/end dates, and optimization goals. Delivery metrics roll up per-package: `impressions`, `spend`, `clicks` — these are reported rollups, not valid `optimization_goals.metric` targets.
### Updating media buys via the campaign update endpoint
Use `PUT /api/v2/buyer/campaigns/:id` with the `mediaBuys[]` array. Allowed actions: `update` (default — modify), `cancel` (cancel running), `delete` (archive).
```json Update budget + optimization theme={null}
{
"mediaBuys": [
{
"action": "update",
"mediaBuyId": "mb_abc123",
"packages": [
{ "packageId": "pkg_1", "budget": 15000, "pacing": "even" }
],
"optimizationGoals": [
{ "kind": "metric", "metric": "completed_views", "target": { "kind": "cost_per", "value": 0.08 } }
],
"updated_reason": "Mid-flight optimization based on early performance"
}
]
}
```
```json Cancel one media buy theme={null}
{
"mediaBuys": [
{ "action": "cancel", "mediaBuyId": "mb_abc123", "reason": "underperforming vs threshold" }
]
}
```
```json Cancel just one package theme={null}
{
"mediaBuys": [
{
"action": "cancel",
"mediaBuyId": "mb_abc123",
"packageIds": ["pkg_2"],
"reason": "format not supported on this device mix"
}
]
}
```
### Campaign membership: which creatives run on the campaign
A creative that is a member of a campaign is delivered on every current and future media buy of that campaign whose products accept its format. Membership is one relationship with three ways to set it, all writing the same edge and running the same seller sync:
* `save_campaign` with `campaignId` and `creativeIds`: the full set of up to 200 creatives on the campaign. A creative missing from the list is detached, a new one attached. Omit the field to leave membership unchanged; it is not accepted on create.
* `save_creative` with `creativeId`, `advertiserId`, and `campaignIds`: the same edge from the creative's side.
* The Creative Library picker under a creative's **Used in** section, which shows the seller-acceptance check per campaign before you choose.
Every creative must belong to the campaign's advertiser. When the campaign has media buys and no seller's products accept a creative's format, the save fails and names those sellers (the first five, with a count of the rest); a campaign with no media buys yet accepts any complete creative, while a media buy whose products declare no accepted formats cannot be checked, so the save fails closed and names it. A creative live on an active or paused media buy cannot be detached until that buy ends. Every way of changing membership, including the Library picker and the singular `save_creative` association, shares one lock protocol, so concurrent membership writes to the same creative or campaign do not overlap: a write that finds another one in progress, or whose target set changed while it was being prepared, returns `CONFLICT` and should be retried with the full set. Detaches run before attaches; if one edge fails, the response carries `partialWrite: true` with the `attached` and `detached` ids that already applied. Read membership back with `get` on the campaign including `creatives`, or `membership.campaignIds` on the creative.
Per-buy selection is a separate, narrower control: `mediaBuys[].creative_ids` below overrides the campaign membership for one media buy.
### Creative attachment to a media buy
Omitting `creative_ids` leaves a media buy's existing creative assignments unchanged. Pass `creative_ids` on a `mediaBuys[]` entry only when you intend to replace or clear its assignments.
```json Replace creatives for one media buy theme={null}
{
"mediaBuys": [
{
"action": "update",
"mediaBuyId": "mb_abc123",
"creative_ids": ["cr_video_15s", "cr_video_30s"]
}
]
}
```
* **Omit `creative_ids`** to leave existing assignments unchanged; no creative data is fetched, validated, synced, or forwarded.
* **`"creative_ids": []`** explicitly clears all creatives from the media buy.
* Each ID must already be linked to the campaign and match a format accepted by the media buy's products. Otherwise the update fails with a validation error — the field is **not** silently filtered.
* `creative_ids` is only valid with `action: "update"`. It is rejected with `cancel` or `delete`.
### Cascade behavior
* **Campaign pause** halts every media buy and package
* **Campaign reactivate** brings them back to ACTIVE (post-flight if dates allow)
* **Creative manifest update** re-syncs to every media buy that uses the format
* **Property list update** propagates to packages without a full re-execute
* **Frequency caps on the campaign or advertiser** are enforced across all media buys
### Failures and debug
When a media buy fails to execute (publisher rejection, ADCP error), `POST /api/v2/buyer/campaigns/:id/execute` returns structured `errors[]`:
```json theme={null}
{
"success": false,
"errors": [
{
"mediaBuyId": "mb_abc123",
"salesAgentId": "agent_xyz",
"message": "Sales agent returned 422: invalid format for product"
}
]
}
```
Pass `debug: true` on execute to include the full ADCP request, response, and A2A debug logs in each error entry.
## Pacing periods
`pacingPeriods` defines time-windowed spend intensity within the campaign flight. Two modes:
* **`weight`** — relative weights (e.g. 3.0 = 3x normal); budget is distributed proportionally
* **`budget`** — explicit dollar amount per period
Gaps between periods are treated as **pauses** (no spend). On execute, each product becomes one package per period with proportional budget.
Pacing periods can only be modified on DRAFT campaigns. After execution, the package split is locked in. See the [pacing periods workflow guide](/v2/guides/pacing-periods) for full mode-by-mode examples.
## Frequency caps
Buyer-side frequency caps configured on the campaign are enforced by Apostra across **all** publishers in the campaign — distinct from publisher-side caps that live in package overlays.
```json theme={null}
"frequencyCaps": [
{ "max_impressions": 5, "window": { "interval": 7, "unit": "days" } }
]
```
When `frequencyCaps` is provided on update, it replaces all existing non-archived caps. See [Frequency caps](/v2/guides/frequency-caps) for the full configuration surface.
## Click-URL parameters
A campaign can add or override the query parameters Apostra appends to a
creative's click-through URL at seller sync. The field has two shapes:
* **Write** (create and update request bodies): an object, `params` plus
`deleteMissing`.
* **Read** (the campaign resource): an array of resolved entries, each
`{ paramKey, paramValue, source }`.
Send the write shape on create or update:
```json theme={null}
"utmConfig": {
"params": [
{ "paramKey": "utm_campaign", "paramValue": "spring-launch" },
{ "paramKey": "utm_content", "paramValue": "{CREATIVE_ID}" }
],
"deleteMissing": false
}
```
`deleteMissing: false` (the default) adds or updates the given keys and keeps
the campaign's other keys; `deleteMissing: true` replaces the campaign's whole
set. A campaign carries at most 50 keys: a request that would leave more is
rejected, so remove keys or replace the set. Reads return the resolved array,
for example:
```json theme={null}
"utmConfig": [
{ "paramKey": "utm_campaign", "paramValue": "spring-launch", "source": "campaign" },
{ "paramKey": "utm_source", "paramValue": "acme", "source": "advertiser" }
]
```
A campaign key wins over the advertiser key with the same name.
V3 `save_campaign` writes the same rows through `tracking.macroAdditions`, where
a `null` value suppresses an inherited key. The inheritance, macro, and
no-defaults rules are in the advertiser guide's
[Click-URL parameters](/v2/object-guides/advertiser#click-url-parameters).
## Routing compatibility metadata
`DECISIONED` / `ROUTED` is a temporary, server-derived billing observation on a
media buy. It exists only for legacy rate-card and ledger compatibility and is
derived from the authoritative AdCP BillingParty fact. It is never a client
input and there is no campaign-level or storefront-level routing type.
Do not use it to infer execution path, optimization, credentials, product
selection, campaign mode, BYOA, AdCP connectivity, targeting support, or
eligibility. Use the explicit field for each concern: seller capabilities,
authentication state, campaign configuration, and BillingParty. The deprecated
`supportedRoutingTypes` v2 storefront field returns both legacy values as a
behavior-free compatibility placeholder.
## Related concepts
Manifest-based creatives uploaded under the campaign
Time-windowed spend intensity
Buyer-side cross-publisher caps
# Catalog
Source: https://docs.apostra.com/v2/object-guides/catalog
Your product list as a feed — so ads can show the real things you sell
## Overview
A **catalog** is an advertiser's structured list of what it sells — products,
offerings, job openings, hotels, vehicle or real-estate listings — made
available to the platform as a feed. Once a catalog is synced, sellers can
build ads from the actual items: a product carousel showing four of your
dresses with live prices and photos, instead of one static banner. Catalog
data also closes the measurement loop — conversion events reference item IDs
from the catalog, so purchases attribute back to the items that were shown.
You don't build anything to have a catalog. If you run a store, you already
have one — it's your product list. Getting it onto the platform is a matter of
exporting or pointing at a feed you already have.
## Getting your feed
A catalog is sourced one of two ways — a remote `url` the platform re-fetches
on a schedule, or `items` supplied inline in the sync request. Each catalog
uses one or the other, never both.
Two options, no app required:
* **Product export CSV** — in the Shopify admin, go to **Products →
Export**, choose **All products** and **CSV for Excel, Numbers, or other
spreadsheet programs**. Shopify emails you a download link. That CSV is a
supported feed as-is with `feed_format: "shopify"` — the standard export
columns (`Handle`, `Title`, `Vendor`, `Variant Price`, `Image Src`, …)
parse directly. Supply the rows as inline `items`, or host the file at a
URL.
* **Public JSON feed** — many Shopify stores expose their products at
`https://your-store.com/products.json`. If yours does, use that as the
feed `url`; a JSON feed with a top-level `products` array parses
natively.
If you already run a feed app (for example the Google & YouTube sales
channel), you can reuse the URL it generates — but note those apps emit
Google Merchant Center format, so set `feed_format:
"google_merchant_center"`, not `"shopify"`.
Point the feed `url` at your existing Merchant Center feed (XML or CSV)
with `feed_format: "google_merchant_center"`.
Reuse your Facebook catalog feed URL with `feed_format:
"facebook_catalog"`.
LinkedIn Jobs XML feeds are supported with `feed_format: "linkedin_jobs"`
and `type: "job"`.
`feed_format: "custom"` accepts JSON, XML, or CSV. Non-standard field
names can be normalized with feed field mappings.
## Key fields
| Field | Type | Notes |
| - | - | - |
| `catalog_id` | string | Your identifier for the catalog — creatives and campaigns reference it |
| `type` | enum | What the items are: `product`, `offering`, `job`, `hotel`, `flight`, `vehicle`, `real_estate`, `education`, `destination`, and more |
| `url` | string | Remote feed URL — provide this **or** `items` |
| `items` | array | Inline feed rows (max 10,000) — provide this **or** `url` |
| `feed_format` | enum | `shopify`, `google_merchant_center`, `facebook_catalog`, `linkedin_jobs`, `custom` |
| `update_frequency` | enum | How often a URL feed is re-fetched: `realtime`, `hourly`, `daily`, `weekly` |
| `conversion_events` | array | Event types (e.g. `purchase`, `add_to_cart`) attributed to items in this catalog |
## Lifecycle
1. **Sync** — push the catalog with
[Sync catalogs](/v2/buyer/advertisers/tasks/sync-catalogs). Up to 50
catalogs per call; the response reports per-catalog `action`
(`created` / `updated` / `unchanged`) and per-item review counts.
2. **Review** — items are validated and content-reviewed. Rejections come back
in `item_issues` with a field-level reason (for example an unreachable
image URL), so you can fix the feed and re-sync.
3. **Use** — campaigns and creatives reference the catalog by `catalog_id`;
catalog-driven formats render items dynamically.
4. **Refresh** — URL feeds re-fetch on their `update_frequency`;
[Refresh catalog](/v2/buyer/advertisers/tasks/refresh-catalog) re-fetches
on demand and creates a new version when the data changed.
Beyond referencing items in creatives, a catalog can drive campaign creation
itself: **catalog activation** fans items out into campaign groups, budget
hints, and creative prompts via a saved transform. See
[Save catalog transform](/v2/buyer/advertisers/tasks/save-catalog-transform)
for the transform → preview → execute → refresh flow. The whole flow is also
available in the UI under **Data sources → Catalogs**.
## V3 catalog tools
Buyer agents can use `save_catalog` to create, replace, or archive one
catalog. The same declaration is available through `search(kind: "catalog")`
and `get(kind: "catalog", id, advertiserId: "12345")`. A `get` request can
include a bounded page of items with `include: ["items"]`; item filters support
IDs, product GTINs, tags, category, `text` (text contained anywhere in an item),
and `status: "approved"`. Use the returned cursor for another page rather than
requesting an unbounded feed. A catalog reads back with the same flat fields
`save_catalog` takes: the feed source (`url`, `feedFormat`, `updateFrequency`,
`feedFieldMappings`) and the selection filters (`ids`, `gtins`, `tags`,
`category`, `query`) sit at the top level, not in a wrapper.
`save_catalog` requires an idempotency key. A repeated completed request with
the same key returns its original result for 24 hours, so a retry does not
create another catalog update. URL catalogs are fetched again when saved;
archive a catalog with `isArchived: true` and the same `catalogId` and
`advertiserId` instead of using a separate delete tool. The MCP request is the
catalog object itself — do not wrap it in `catalog`:
```json theme={null}
{
"catalogId": "summer-sale-2026",
"advertiserId": "12345",
"name": "Summer Sale 2026",
"type": "product",
"url": "https://feeds.acme.com/products.xml",
"feedFormat": "google_merchant_center",
"updateFrequency": "daily",
"idempotencyKey": "save-summer-sale-2026-0001"
}
```
## Related
Push catalog feeds and items to an advertiser
See synced catalogs and their status
Fan catalog items out into campaigns
Catalog and catalog activation, defined
# Creative
Source: https://docs.apostra.com/v2/object-guides/creative
Manifest-based creative assets, nested under campaigns, with format coverage and per-product propagation
## Overview
A **Creative** in v2 is a **creative manifest** — a structured bundle containing uploaded assets (images, video, HTML, VAST tags), an optional brief, brand info auto-resolved from the campaign's advertiser, and a target ADCP format. Delivery is campaign-scoped: a manifest reaches a media buy through the campaign it is attached to.
**New in v2**: the campaign is the default home for a creative. Creatives are **created directly under a campaign** at `/api/v2/buyer/campaigns/:campaignId/creatives/...`, rather than being maintained as an advertiser-level catalog that campaigns reference. The model is also fully manifest-aligned with the [AdCP creative manifest spec](https://adcontextprotocol.org/schemas/latest/core/creative-manifest.json).
**Advertiser-level creatives still exist**, as a staging and reuse shelf rather than the delivery model. You can [create a master under an advertiser](/v2/buyer/creatives/tasks/create-advertiser-creative-manifest) before any campaign exists, list an advertiser's creatives, and promote a creative to the advertiser library — see [Advertiser-scoped creative listing](#advertiser-scoped-creative-listing) and [Advertiser library: assign and unassign](#advertiser-library-assign-and-unassign) below. An advertiser-level master carries `advertiser_id` with no `campaign_id` and never delivers until it is assigned to a campaign.
**Field-name exception**: Creative manifests use **snake\_case** field names (`creative_id`, `campaign_id`, `format_id`, `format_kind`, `format_option_ref`, `template_id`, `brand_domain`, `target_format_ids`) because they are passed through verbatim to the ADCP wire-format. This is an intentional exception to v2's general camelCase convention — see the [migration guide](/v2/setup/migration-guide) for context.
## Why this changed
V1 treated the advertiser-level catalog as the primary home for a creative, reused across campaigns. V2 makes the campaign the default home so:
* Each campaign sees exactly the formats its selected products require (`creativeFormats.required`)
* Asset uploads happen in a single multipart request with the manifest metadata
* Format and template auto-detection runs against the campaign's product mix
* Tracking macros and brand info are resolved from the campaign + advertiser context
The result is fewer mismatches between selected products and uploaded creatives, and a cleaner per-campaign creative library.
## Key fields
| Field | Type | Notes |
| - | - | - |
| `creative_id` | string | Manifest ID |
| `preview_revision` | string | Exact opaque revision of the buyer-preview projection |
| `campaign_id` | string | Owning campaign |
| `name` | string | Human-readable name |
| `message` | string | Optional creative brief / direction text |
| `template_id` | string | Creative template (`display_image`, `display_html`, `video_standard`, vendor tags, etc.) |
| `format_id` | object | `{ agent_url, id }` — **legacy** (v1 named-format) selector. New create input uses either this field or canonical identity, never both. A persisted imported manifest may retain it as provenance alongside `format_kind`; in that case the canonical identity is authoritative and the URL is not dispatched. |
| `format_kind` | string | **Canonical** (AdCP 3.1) format selector — a bare enum (`image`, `video_vast`, `video_hosted`, …) with **no `agent_url`**. It may be accompanied by `format_option_ref`. Video/audio duration rides in `params` (`duration_ms_exact` / `duration_ms_range`), not on `format_id`. See the [glossary](/v2/concepts/glossary#f). |
| `params` | object | Validated parameters retained with canonical identity. A URL-free `image` response includes positive whole-number `width` and `height` here and omits a synthetic `format_id`. |
| `format_option_ref` | object | Product- or publisher-scoped canonical option `{ scope, format_option_id, publisher_domain? }` that accompanies `format_kind`. It contains no agent URL and is mutually exclusive with a legacy outbound `format_id` selector. |
| `target_format_ids` | array | Formats from campaign products this manifest can serve |
| `brand_domain` | string | Auto-resolved from the campaign's advertiser |
| `assets` | array | Uploaded assets (id, type, MIME, public URL, source) |
| `preview_url` | string | Manifest-local processed preview URL; provider placement previews use the separate preview action below |
| `tracking` | object | Templates of the auto-generated impression and click trackers (`impression_tracker_url_template`, `click_tracker_url_template`), plus `supported_macros`. Templates are not trafficking-ready: see [Tracker templates](#tracker-templates) |
| `sync_status` | object | `{ synced, agent_count, last_synced_at }` across sales agents |
| `frequencyCaps` | array | Buyer-defined caps on this creative |
| `labels` | object | Dimension values on the Creative, keyed by dimension key. `tags` is the built-in open dimension. |
## Asset types
V2 assets cover the full ADCP type spectrum:
| Type | Purpose |
| - | - |
| `IMAGE` | JPEG, PNG, GIF, WebP, SVG |
| `VIDEO` | MP4, WebM, QuickTime |
| `AUDIO` | MP3, WAV, OGG |
| `HTML` | Inline content or hosted URL |
| `JAVASCRIPT` / `CSS` | Companion script and style assets |
| `VAST` | VAST tag (URL or inline XML) — **renamed from v1's `VIDEO_VAST`** |
| `ZIP` | Inspected HTML5 bundle, stored intact with its entry point and declared size |
| `URL` | Clickthrough, tracker pixel, or VAST URL with explicit `url_type` |
| `FONT` / `LOGO` / `DOCUMENT` / `TEXT` | Supporting assets |
URL assets are typed via `url_type`: `CLICKTHROUGH`, `TRACKER_PIXEL`, `TRACKER_SCRIPT`, or `VAST`. Webhook assets enable server-side dynamic content rendering (DCO) with HMAC or API-key signing.
### Uploaded assets and assembled Creatives
The upload Task and the Creative library hold different things. The Task puts private JPEG, PNG, or MP4 bytes into managed storage. During transport it uses an expiring opaque `asset_ref`; successful advertiser-scoped finalization requires and returns a durable `asset_id` for the advertiser's asset library and retains the original filename as its display name. After finalization, the Task records a conversation receipt with the asset ID when one exists, saved name and tags, content type, byte size, digest, and any verified video facts. For a provider-scoped upload without a reusable Library asset ID, the digest is byte identity for the receipt only: it is not an `assetId` or `sourceUid`, cannot be passed to `save_creative` or provider adoption, and does not imply that later attachment is possible. On external hosts, that Task stays open after the receipt so its private continuation reference remains available for any separately requested follow-up; close the Task when no follow-up is needed. The receipt reports the upload only; creating or attaching a Creative, syncing data, contacting a provider, and delivering media still require a separate request. The transport `asset_ref`, provider identities, signed upload URLs, storage paths, `delivery_url`, and `source_uid` are excluded from the receipt.
### Browse an advertiser's Creative Library
In Apostra, select an advertiser and then choose **Manage → Creatives**.
You can also ask Murph to **show me my creatives** after selecting the
advertiser. Both paths open the same Creative Library and show the advertiser
name and ID at the top. The Library stays within that advertiser if the
surrounding workspace changes or you reload the page.
The Creative Library opens in a list grouped by Creative format, with
Creatives that do not have a format in **Uncategorized** at the end of the
list. Choose the grid toggle to switch layouts for the current browser
session. If no loaded Creative has a format, the Library shows one ungrouped
list and does not offer format grouping.
Select a Creative or Asset to open a previewer beside the list on wide layouts
and in a sheet on narrow ones. The list keeps its search, filters, grouping,
and scroll position while the previewer shows the available preview, summary
facts, and attachment choices. Close it with **Escape** or **Back to creatives**
to continue browsing.
Opening Creatives from a campaign or from a link shared by your agent opens the
same Library with that campaign selected. Existing Campaign Creative Assets
bookmarks also move to this shared Library instead of opening the retired
screen. Apostra restores the advertiser and campaign when you reload a
valid link. A campaign or Creative that does not belong to the selected
advertiser, or that you cannot access, is refused rather than opened under a
different advertiser.
Campaign focus lists the Creatives currently attached to that campaign and
supports text search. Choose **All advertiser Creatives** to browse reusable
Creatives, open one, and attach it to the selected campaign. **Creative
requirements** opens the campaign's current format requirements and mapping
task. Those requirements come from the signed format declarations on the
campaign's selected Products. Uploading a file still creates an independent
Library asset; you can then assemble a Creative from that asset and attach the
saved Creative to the campaign. Preview remains tied to the exact saved
Creative revision.
Use **All Creatives** for every visible, non-archived Creative owned by the
advertiser. Use **Promoted shelf** for the smaller set marked for reuse. The
current view, search text, number loaded, and total remain visible. If more
results are available, the Library says the list is incomplete and lets you
load the next page.
Opening a Creative loads its exact saved revision through Apostra's preview
origin. If the Creative changes, refresh the Library before opening the new
revision. **Upload asset** starts the existing upload Task; completing that Task
adds an asset, not an assembled Creative. Product selection remains required
before assembling a new Creative.
The **Catalogs** tab lists the advertiser's connected catalog feeds beside
Creatives and Assets. Each card shows its item count, source, most recent sync,
and whether the feed is Healthy or Stale (with the reason). **Connect a
catalog** and **Open** take you to the existing catalog manager; **Build
creatives** is shown as a forthcoming workflow and does not start work yet.
For an object response instead of the visual Library, use V3
`search({kind: "creative", filter: {advertiserId}})`.
Use V3 `search({kind: "creative_asset", filter: {advertiserId}})` to find active assets, optionally filtering by text, media kind, or `labels`. Both Creative and asset reads return `labels`; `filter.labels` matches every named dimension and any supplied value within each dimension. Dimension names and their allowed values belong to the buyer account, so a teammate who can use that account sees the same vocabulary. Attached labels and search results remain limited to the advertiser in the request. The Library is shared across the customer: anyone with read access to an advertiser can find, attach, and deliver that advertiser's assets. Each asset keeps its original uploader for audit and provenance. `tags` is the legacy asset-column filter; new label writes use the built-in open `tags` dimension and asset finalisation also keeps that legacy column in sync. Label values are slugs: lower-case ASCII letters and numbers, separated by single hyphens, with a maximum of 64 characters. Promoted MP4 records include `verifiedMedia` with the verified container, video codec, audio codec, duration, width, and height for format compatibility checks. Promoted audio records include the verified container, audio codec, bitrate, channel count, duration, and sample rate. They do not expose storage locations or delivery URLs. Images remain capped at 30 MB, WAV and MP3 audio at 50 MB, and MP4 video at 2 GB. Audio is available as an independent Library asset and can be used in `save_creative` with `formatKind: "audio_hosted"` and its asset ID in `sourceAssets`.
Use V3 `search({kind: "creative_format", filter: {advertiserId, productId}})`
before assembly to list canonical format options on the selected product. Pass
the exact opaque Product ID returned by marketplace discovery, including its
`sf2:` prefix. Apostra resolves its storefront, inventory source, and
seller-local product identity without falling back to a matching raw ID.
`productId` is required to keep the catalogue read bounded. You can narrow the
read further with `sellerId` or `formatKind`. The
result includes a signed opaque `id`, the exact `formatOptionRef`, and each declared slot's
identity, required and repeat cardinality, accepted asset kind, and authored
constraints. It also identifies the seller by display name and id. The read
does not require a campaign or destination and makes no provider call or setup
request. Seller-owned product declarations are available without a provider
connection; direct-adapter products keep their existing active-connection
visibility rules. Use the returned opaque id with
`get({kind: "creative_format", id, advertiserId})` to read the same declaration
again. The read excludes legacy global format catalogue entries.
V3 `save_creative` turns asset-library records into one format-bound Creative. For campaign-independent assembly, send `advertiserId`, the selected format `id` as `creativeFormatId`, and `sourceAssets`; each source binding supplies `assetId` and names its required image or video slot. Add `mode: "draft"` to save a partial advertiser-owned assembly before every required slot is ready. Revise it with the same `creativeId`, the current `stateRevision` as `expectedRevision`, the signed `creativeFormatId`, and the complete replacement slot state. Use `mode: "complete"` when the assembly is ready for full format validation. Drafts and completed advertiser assemblies stay outside campaigns and do not contact a provider.
Apostra revalidates the signed format and derives its canonical kind and option on every save. Supply copy and destination slots through `social` and `clickUrl`. A stale revision fails without changing the Creative, and a replacement never silently merges with its prior state. For a campaign-scoped create, send `campaignId`, `formatKind`, and the campaign product's exact `formatOptionRef`; that legacy save also attaches the new Creative to the campaign. Calls that omit `mode` retain complete-save behavior. Apostra rejects expired or foreign assets, stale or mismatched format selections, undeclared slots, wrong media types, duplicate slot values, and missing required slots at completion.
The saved result is the Creative manifest. An advertiser-level assembly appears once under **All creatives** and its authoritative V3 search and readback contain durable asset rows, `state`, the current numeric `stateRevision`, and `membership.campaignIds`. To associate a completed advertiser Creative before a seller package exists, call `save_creative` with its `creativeId`, `advertiserId`, and `campaignId` only. Repeating that call is safe. The association records campaign membership; it does not assign a package, create provider delivery, or create spend. Those are separate later states. To set the full set of campaigns an advertiser Creative runs on, call `save_creative` with `creativeId`, `advertiserId`, and `campaignIds`: the list replaces membership, so a campaign missing from it is detached and a new one attached. Omitting `campaignIds` never detaches. Attaching checks the campaign's sellers: when the campaign has media buys and no seller's products accept the Creative's format, the call fails and names those sellers; a campaign with no media buys yet accepts any complete Creative, while a media buy whose products declare no accepted formats cannot be checked, so the save fails closed. A Creative live on an active or paused media buy cannot be detached until that buy ends. Every way of changing membership, including the Library picker and the singular `campaignId` association, shares one lock protocol, so concurrent membership writes to the same Creative or campaign do not overlap: a write that finds another one in progress, or whose target set changed while it was being prepared, returns `CONFLICT` and should be retried with the full set. The same edge is set from the campaign side with `save_campaign.creativeIds` and from the Creative Library picker; readback is `membership.campaignIds` on the Creative and `creatives` on the campaign. The existing opaque `revision` remains the buyer-preview projection digest. The same completion view can load that exact saved state through **Show preview**. Creative collections contain assembled Creatives, not the independent uploaded assets. V2 MCP sessions can open the same upload Task, but asset search/get and `assetId` assembly are V3-only.
### Preview a saved Creative
In the Creative Library, select **Show preview** to run the current saved
revision in an isolated frame. The frame uses Apostra's render origin. It
does not embed a provider preview or run the Creative in the Library page.
V3 clients can request the same read with
`get({kind: "creative", id, advertiserId, include: ["preview"]})`, or use the
Creative's campaign as `sourceId`. The `preview` field contains only
`renderUrl`, `expiresAt`, the opaque preview `revision`, and the exact current
numeric `stateRevision`. If the saved format
has no first-party renderable asset, it contains a `reason` instead. Existing
`get` calls omit `preview` unless it is included, but every Creative read now
returns the whole-state `stateRevision` and no longer forwards the legacy V2 provider
preview URL or tracker fields.
The buyer preview renders direct image, video, and audio source assets. HTML,
VAST, DAAST, and ZIP sources return a not-renderable reason because their
nested documents can load provider resources or measurement trackers that are
not safe to project into this read.
Preview URLs expire after 15 minutes and stop working sooner if the Creative
changes. Request a new preview after expiry or whenever the returned revision
or `stateRevision` differs from the Creative read. Storage URLs, provider preview HTML, and
measurement trackers are not returned by the V3 Creative read.
A `VAST` url asset also takes `vast_version` — one of `2.0`, `3.0`, `4.0`,
`4.1`, `4.2`, `4.3`. Declare it: a managed ad-server destination checks the version
against the ones it holds delivery evidence for, so a tag without one is not
eligible for those routes and fails creative sync. See
[video creatives](/v2/concepts/video-creatives#declare-a-vast-tags-version).
### HTML5 bundles
Canonical `html5` creatives use one ZIP asset in the `html5_bundle` slot, with
an optional `backup_image`. Buyer chat inspects these packages without running
their JavaScript, preserves the ZIP, and carries the declared dimensions into
seller matching. Generic multi-creative ZIPs continue to expand into separate
creatives. Direct multipart ZIP creation and reclassification are rejected;
use the inspected buyer-chat upload path.
An imported bundle is not automatically delivery-ready. Media-buy sync
requires an explicit landing-page URL, and compatibility fails closed when a
seller declares an HTML5 runtime constraint that inspection cannot verify.
## Lifecycle
`GET /api/v2/buyer/campaigns/:campaignId/creatives/templates` returns available templates plus the selected products' required formats. V2-native declarations are returned in `campaign_format_options`; legacy format references remain in `campaign_format_ids` for compatibility. Each product's requirements are also broken out under `campaign_products`. The legacy named-format registry across agents is available at `GET /api/v2/buyer/formats`.
POST a multipart request with files + JSON metadata. Apostra auto-classifies assets, processes HTML to rewrite local references to CDN URLs, inserts ADCP macros, and generates a preview. If the campaign already has non-draft media buys, a complete new manifest is automatically propagated to their active sellers; no separate `sync_creatives` call is required.
`GET /api/v2/buyer/campaigns/:id` returns `creativeFormats: { required, covered, missing }`. Upload more manifests until `missing` is empty.
On campaign execute, the manifests sync to each sales agent for the formats they support. A ready manifest is registered for delivery in the same transaction as its assets, so it cannot disappear between upload and assignment. Sync status is reflected in `sync_status` on each manifest.
`PUT` the manifest with new files, deletions, or reclassifications. Updates flow back to media buys.
## Media-buy compatibility results
When a media buy inherits creatives from its campaign, Apostra checks each
creative against the selected products before delivery. An inherited creative
that cannot serve those products is omitted and reported in
`creative_compatibility_warnings` with a status of `incompatible` or
`unresolved` and a reason. Older creative records that do not contain a usable
format are reported in `legacy_creatives_excluded`.
Historical static-image labels such as `display_300x250`,
`banner_300x250`, and `display_300x250_nongenerative` can serve a product's
same-sized `display_300x250_image` placement without retyping the saved
creative. Apostra sends the product's advertised format reference and still
checks the creative's asset type and dimensions before delivery. Other
legacy-format differences remain incompatible rather than being widened by
format family.
Apostra preserves an imported legacy format label for traceability, but a saved
creative's canonical `format_kind` is authoritative for campaign and product
matching. New saved creatives must resolve a canonical kind. If an older
creative's assets, media type, dimensions, and import provenance do not identify
one format kind deterministically, Apostra quarantines it for explicit upgrade
instead of guessing. Seller-specific format references are applied only when
the creative is adapted for that seller; they do not change the saved master.
If a create-media-buy request explicitly names an incompatible or unresolved
creative, the request is rejected instead of creating a buy that silently
drops it. Integrations should surface these response fields so a buyer can
replace or correct the creative before launch.
For creatives your integration creates, use the canonical `format_kind` field.
It never contains an agent URL. Apostra only preserves a URL-bearing
`format_id` when an external third-party system explicitly supplied that
legacy reference.
## Conversational creative sessions
Creative sessions are the agent-native path for buyers who want to generate, compare, refine, validate, and save creatives without handling manifest details directly.
### Composer drafts
For a campaign, the Creative Library can open a Composer Task without Creative
Engines. Choose Snap Story, Meta Reels, Display 300×250, or Audio :30, then
fill its copy and uploaded-asset slots, name the draft, add its click-through
URL, and save it. The Task shows which slots are filled and a live preview,
then saves the assembly draft in the campaign's Creative Session without
selecting or calling an engine.
Use the host chat to request changes. The Task does not provide a second change
bar and reloads the session after chat updates it. Saving a composer draft keeps
work in review; it does not create a campaign Creative, attach it to delivery,
or make it live. When Creative Engines is not enabled, the Task keeps assembly
controls available and labels generative controls unavailable. Ask your Apostra
account team to enable it for the Buyer Account; generation also needs a
connected engine and applicable provider setup.
The intended user flow is:
1. **Idea** — the buyer describes the channel, audience, goal, and source assets.
2. **Draft gallery** — a creative adapter returns multiple draft variants for human review.
3. **Refine** — the buyer gives natural-language feedback like "make the book more prominent" or "less busy."
4. **Evaluate** — draft checks are advisory; final hard/blocking checks stop finalization.
5. **Finalize** — the approved leaf is saved as a campaign creative manifest.
When Apostra generates directions in separate calls and the request includes
`request.idempotency_key`, give each direction a distinct short label. Provider
keys use the first 40 characters after lowercasing, replacing characters outside
`a-z` and `0-9` with hyphens, and trimming edge hyphens. Labels such as
`Summer sale` and `summer-sale` therefore collide. Apostra rejects that batch
before calling a generator; change the labels before retrying. Existing keys
for unambiguous directions remain unchanged.
Use `POST /api/v2/buyer/campaigns/:campaignId/creative-sessions` to start a session. The response is designed for MCPUI and includes the draft `variants[]`, `asset_store`, `renderer_capabilities`, `rendering_model`, staged evaluator badges, and `format_renders[]` once a final is approved.
When refining a variant, send only the `request` settings you want to change for
that refinement. Omitted settings retain the session's saved values. Nested
objects merge; arrays and scalar values replace the supplied field. Explicit
`null` values in `config` are forwarded to the provider for validation. A string
`creative_brief` replaces the brief used for that refinement. Provider setting
overrides do not change the session defaults, and the selected variant remains
the parent.
If a finalization response is lost, read the session before retrying. Once the
session reports `finalized`, repeating the same selected variant, name, and
message returns its saved result without creating another campaign creative.
Retries still require current campaign write access. Different inputs return a
conflict; use the saved `creative_id` to read or update the campaign creative.
For older finalized sessions that did not retain the original request, retry
without name or message overrides.
This retry behaviour applies after the finalized session has been saved. It
does not guarantee recovery from an interruption between creative creation and
session persistence, or from concurrent finalization requests. A session that
does not report `finalized` is not proof that no creative was created.
Creative sessions are review workflows. They do not replace campaign creative manifests. Finalization is the transition point where a selected draft/refinement becomes a normal campaign creative with `creative_id`, legacy or canonical format identity, assets, preview, and sync status.
### Required placement shapes
When a campaign has required placements, a `format_renders[]` tile is produced per placement at its real size. A placement with an extreme aspect ratio — a wide leaderboard or a tall skyscraper — only renders at its true shape if the generator can compose that ratio natively. Gemini and FAL do; OpenAI generates a fixed set of shapes and crops to fit.
If your session's connected generator would crop such a placement and you have also connected a native-ratio generator (Gemini or FAL) for that advertiser, that one placement is rendered on the native-ratio generator instead — on your own connected key for it, billed to that vendor account, the same as any generation on that provider. Other placements in the session stay on your session generator. When no native-ratio generator is connected, the placement still renders on the session generator with an advisory note that it may crop; connect one to fix the shape.
## Asset lifecycle
Creative production uses three related but distinct asset layers:
| Layer | What it is | Stored as | How to use it |
| - | - | - | - |
| **Source asset** | Buyer-supplied input such as a book cover, product photo, logo, DAM URL, catalog image, audio file, or reference image | `source_asset` and `asset_store.assets[]` inside the creative session | Treat as raw input. Preserve locked assets exactly; track role, provenance, rights, checksum, dimensions, and render crop/mask metadata when available. |
| **Draft variant** | A generated or composited creative direction for review | `variants[]` with preview URL, quality, status, lineage, and evaluator checks | Use for exploration. Drafts may be discarded, branched, or refined. They are not campaign creatives until finalized. |
| **Final creative** | The approved execution saved to the campaign | Creative manifest under `/campaigns/:campaignId/creatives` | Use for delivery. It carries the final render, linked assets, text assets, ADCP format identity, tracking, preview, and sync status. |
For locked product, package, logo, or book-cover assets:
* Set `locked_asset: true`, `can_transform: false`, `role: "product"` (or the closest role), and specific `preservation_notes`.
* Keep provenance separate from rights. `source: "upload"` or `source: "dam"` says where the asset came from; `rights.status` says whether it is cleared.
* `rights.status: "unknown"` is a non-blocking review warning for final approval. `restricted` or `expired` blocks finalization.
* If the source image includes studio background or whitespace, provide `render_crop`, `subject_bounds`, or a transparent cutout/mask when available. Cropping is a preservation-safe fallback; a real cutout produces cleaner final compositions.
### Asset URLs must be publicly fetchable
Every asset URL on a finalized creative manifest is forwarded verbatim to the seller's ad server, which fetches it from its own infrastructure with no Apostra credentials. So a manifest asset URL has to be a publicly reachable `https://` (or `http://`) URL — those are the only two schemes accepted when you submit one.
Apostra-internal storage locations are not deliverable, even though they resolve for you while you are authoring. That includes `gs://` object URIs, `scope3-://` internal handles, and `https://storage.googleapis.com/...` URLs that address an Apostra-managed private bucket. These are refused wherever they appear.
Schemes other than `http(s):` — `tel:`, `market://` and the like — are refused too, both at upload and whenever the creative is **rebuilt** from its manifest for delivery. A manifest row that already stores one is not re-refused on a path that forwards the stored payload as-is, so an existing deep link keeps working there. Those refusals name the scheme, so you can tell them apart from a storage refusal.
The check runs when the creative is projected for delivery, so it can surface on `sync_creatives`, on media-buy execution, and on a media-buy update that assigns creatives — not only at upload time.
If an asset is not deliverable you get a `422 INVALID_REQUEST` naming the refused creative:
```json theme={null}
{
"error": {
"code": "INVALID_REQUEST",
"field": "cr_9f2a",
"message": "Creative cr_9f2a references storage that cannot be delivered to a destination. Publish the offending asset at a public HTTPS URL and retry."
}
}
```
The subject is the creative when the refusal comes from syncing or executing an
existing creative, and the individual asset (for example `hero-2024`) when it
comes from building the creative in the first place. If your id contains
characters we do not echo back — a dot or a space, say — the message says
"A creative on this request…" instead of naming it, and `field` reads
`creative`.
**To fix it**, publish the asset at a public HTTPS URL you control (or a CDN), then update the manifest asset to point at that URL and retry.
The message deliberately does not say *which* storage location was detected, and on the sync/execute path it names the creative rather than the individual asset slot. If you cannot tell which asset in a multi-asset manifest was refused, contact support — Apostra retains the exact slot and the specific reason in server-side logs and can tell you which one to fix.
## Seller-specific versions
The campaign's selected products determine which seller formats need coverage. Use the creative template/format endpoints before and after finalization:
1. `GET /api/v2/buyer/campaigns/:campaignId/creatives/templates` shows required campaign formats and available templates.
2. Start a creative session against the best matching creative adapter and target format.
3. Finalize the chosen direction into a creative manifest.
4. Re-check `creativeFormats` on `GET /api/v2/buyer/campaigns/:campaignId`.
5. If `creativeFormats.missing` still contains seller formats, generate or adapt seller-specific manifests for those formats.
Seller-specific versions should keep a common creative direction and source-asset lineage, but each saved manifest must match the actual seller format it covers. For example, the same "Wimbledon book hero" direction may need separate square image, portrait story, audio, video, or HTML executions depending on the selected products.
`format_renders[]` is the review surface for publisher/format previews. It helps a buyer inspect what the final creative looks or sounds like in a particular canonical renderer. The actual delivery object remains the saved creative manifest and its synced seller-specific format coverage.
## Performance loop
Creative work does not end at finalization. The full performance cycle is:
1. **Create or update campaign goals** — use `performanceConfig` on the campaign when optimizing toward events or metrics.
2. **Launch with format coverage** — execute only after required creative formats are covered.
3. **Collect delivery and outcome data** — use reporting plus conversion/measurement ingestion (`log_event`, measurement data sync, or measurement records depending on integration).
4. **Compare creative performance** — inspect performance by campaign, media buy, package, and creative where reporting dimensions are available.
5. **Iterate** — duplicate or start a new creative session from the winning direction, then refine the creative based on observed performance and seller feedback.
The Anjali/The Shift prototype exercises the creative build, preview, evaluator, and finalization loop. A complete performance cycle also needs live delivery, measurement ingestion, reporting by creative, and an optimization decision that feeds a new creative iteration.
## Evaluator strategy
Creative evaluators answer different questions at different moments. Treat them as a selected set of checks, not a single score.
| Evaluator type | Question it answers | Typical stage |
| - | - | - |
| Core readiness | Is there a renderable preview and valid manifest structure? | Draft, final |
| Asset fidelity | Did locked assets stay intact, and are rights/provenance usable? | Draft, final |
| Brand/brief | Does the direction fit the campaign brief and brand guidance? | Draft, final |
| Format/seller | Does this execution satisfy the selected seller or publisher format? | Final |
| Policy/safety | Is there policy, claims, legal, or safety risk? | Final |
| Performance | Did it actually work after delivery? | Postflight |
Draft evaluator warnings should guide refinement. Final hard/blocking failures prevent finalization. Non-blocking final warnings, such as unknown source-asset rights, should be shown as review notes. Performance evaluators should only run after delivery/reporting data exists; pre-flight checks cannot prove a creative will win.
Longer term, evaluator selection should come from the creative session context:
* source assets and lock/rights metadata
* modality and renderer type
* campaign brief and brand guidance
* selected seller formats and publisher policies
* available adapter/seller evaluator capabilities
* postflight reporting and measurement data
## Common operations
### Create a creative manifest (multipart)
The endpoint accepts a `metadata` JSON field plus `files[]` form fields:
```bash curl theme={null}
curl -X POST https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/creatives/create \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: multipart/form-data" \
-F 'metadata={
"name": "Q2 hero video",
"message": "30s product reveal — premium tone, 2-second hook",
"template_id": "video_standard",
"url_asset": { "url": "https://acme.com/promo", "url_type": "CLICKTHROUGH" },
"assets": [
{ "filename": "hero-30s.mp4", "asset_type": "VIDEO", "label": "main_video" }
]
};type=application/json' \
-F "files=@hero-30s.mp4" \
-F "files=@companion-banner.png"
```
```bash With a VAST tag theme={null}
curl -X POST https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/creatives/create \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: multipart/form-data" \
-F 'metadata={
"name": "VAST premium",
"template_id": "video_vast",
"url_asset": {
"url": "https://ad.doubleclick.net/ddm/vast/...",
"url_type": "VAST",
"vast_version": "2.0"
}
};type=application/json'
```
Creative creation is a single multipart request: a JSON `metadata` field plus zero or more `files` parts (`-F "files=@..."`). The server accepts up to the configured `MAX_FILE_COUNT` files per request and pairs each file with the matching `assets[].filename` in the metadata.
File limits: 50 MB per file, 20 files per request. Allowed MIME types include images (JPEG, PNG, GIF, WebP, SVG, HEIC), video (MP4, WebM, QuickTime), audio (MP3, WAV, OGG), HTML/CSS/JS, fonts, PDF, and XML/markdown.
### List manifests
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/creativeManifest?take=50" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Filters: `search`, `includeFrequencyCaps`. Pagination: `take`, `skip`.
### Get / update / delete
```bash theme={null}
curl https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/creatives/cm_abcdef \
-H "Authorization: Bearer $SCOPE3_API_KEY"
curl -X PUT https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/creatives/cm_abcdef \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-F 'metadata={
"delete_asset_ids": ["asset_old"],
"new_assets": [{ "filename": "v2.mp4", "asset_type": "VIDEO" }]
};type=application/json' \
-F "files=@v2.mp4"
curl -X DELETE https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/creatives/cm_abcdef \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
### Render an ADCP preview
```bash theme={null}
curl -X POST https://api.apostra.com/api/v2/buyer/campaigns/cmp_987654321/creatives/cm_abcdef/preview \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Calls the configured ADCP creative agent or platform adapter's
`preview_creative` tool. Provider-backed renders are returned as external links
and are never executed inline. Availability and fail-closed behavior vary by
platform; see [Creative preview support](/v2/guides/ad-platform-compatibility/creative-preview).
## Advertiser-scoped creative listing
`GET /api/v2/buyer/advertisers/:advertiserId/creatives` returns **all creatives scoped to the advertiser** — both promoted library creatives (`evergreen`/`reference` role) and flight-specific creatives attached to any campaign under this advertiser. A creative inherits the campaign's `advertiser_id` at creation time, so it appears in this list regardless of campaign attachment.
Use `?promoted=true` to filter to library creatives only (those explicitly saved with a role). Without this filter, the full set is returned — including creatives uploaded via the campaign-level creative dashboard that have not been promoted.
The advertiser creative listing also accepts `?search`, `?role`, `?source`, `?format_kind`, `?asset_type`, and `?dimensions` filters alongside standard `?take`/`?skip` pagination.
## Advertiser library: assign and unassign
Creatives can be saved to an advertiser library (promoted to `evergreen` or `reference` role) and then attached to campaigns via the advertiser-scoped endpoints:
| Method | Path | Purpose |
| - | - | - |
| `POST` | `/api/v2/buyer/advertisers/:advertiserId/creatives/create` | [Create a master directly under the advertiser](/v2/buyer/creatives/tasks/create-advertiser-creative-manifest), with no campaign |
| `POST` | `/api/v2/buyer/advertisers/:advertiserId/creatives/save-to-library` | Promote creatives to the advertiser library with a role |
| `POST` | `/api/v2/buyer/advertisers/:advertiserId/creatives/:creativeId/campaigns` | Assign a library creative to a campaign |
| `DELETE` | `/api/v2/buyer/advertisers/:advertiserId/creatives/:creativeId/campaigns/:campaignId` | Remove a library creative from a campaign |
### Ownership validation
Both assign and unassign enforce advertiser ownership: the creative and the target campaign must belong to the same advertiser. If they belong to different advertisers, the endpoint returns **HTTP 422** with error code `INVALID_REQUEST`.
```json theme={null}
{
"error": "Creative belongs to a different advertiser than the campaign.",
"code": "INVALID_REQUEST",
"field": "advertiserId"
}
```
A creative with no `advertiser_id` (not yet scoped to an advertiser) can be assigned to any campaign, and the backfill sets its `advertiser_id` to match the campaign's.
### Collection fan-out
When you assign a collection to a campaign, members whose `advertiser_id` conflicts with the campaign's advertiser are silently skipped. The response includes an `ownership_excluded` count so you can detect this. Check that all expected creatives appear in the campaign after a collection assign, and promote any skipped creatives to the correct advertiser before retrying.
## Advertiser creative collections
An advertiser can group its saved creatives into named collections that live
with the advertiser rather than with a campaign. A collection is
non-executable: it holds creatives, it does not deliver them. Attach a
collection from a campaign to make its creatives available there. Changes to a
collection carry a revision check so concurrent edits fail loudly instead of
overwriting each other. In the Creative Library page, creatives and collections
show in a grid or a list, switchable from the toolbar, with the same rows,
search, and filters in both. See
[Manage advertiser creative collections](/v2/buyer/creatives/tasks/manage-advertiser-creative-collections).
## Format coverage on the campaign
When you `GET /api/v2/buyer/campaigns/:id`, the response includes:
```json theme={null}
"creativeFormats": {
"required": [{ "format_kind": "video_hosted" }],
"covered": [{ "format_kind": "video_hosted" }],
"missing": []
}
```
`missing` is the prompt to upload more creatives. Coverage is computed against the formats every selected product needs.
## Tracking macros
When the format supports it, Apostra auto-injects:
* An **impression tracker** URL as a `TRACKER_PIXEL` URL asset
* A **click tracker** URL that wraps the original clickthrough — the original is preserved in the `rurl` query parameter
When the hosted tag wrapper is available in an environment, VAST and DAAST
tags are sent to sellers as a hosted VAST or DAAST wrapper on our own origin.
The wrapper follows the buyer-supplied tag and carries our impression, click,
and playback trackers for start, first quartile, midpoint, third quartile, and
completion. Those trackers are not also sent as separate assets, so an
impression is counted once. Until then, VAST and DAAST tags keep their existing
seller payload, including their separate tracker assets.
Apostra resolves identity macros when the creative is synced for a media buy,
including a campaign creative sync, which fills each media buy's own ids. When
you edit a creative that is assigned to a live media buy, the update reaches
that seller once per assigned media buy, routed to that buy's inventory and
filled with that buy's ids, so an edit never replaces a seller's filled copy
with placeholder ids.
It fills `{MEDIA_BUY_ID}`, `{CREATIVE_ID}`, and `{CAMPAIGN_ID}` in Apostra tracker
URLs and in the [click-URL parameters](/v2/object-guides/advertiser#click-url-parameters)
resolved from the advertiser, campaign, and creative layers. In tracker URLs
`{PACKAGE_ID}` resolves to the package when the sync places the creative in
exactly one package of that media buy, and to an empty value otherwise, because
one synced creative can be assigned to multiple packages. In click-URL parameters the media-buy value is
the storefront transaction id used for attribution; a parameter whose identity
macro has no value for that sync is omitted instead of carrying a different id.
How click-URL parameters treat macros the seller fills, such as `{DOMAIN}`, is
described in [Click-URL parameters](/v2/object-guides/advertiser#click-url-parameters).
The same identity macros written directly into your click-through URL are filled
in the destination the click tracker carries in `rurl`, so the shopper reaches
your landing page with real ids rather than placeholder text. There,
`{PACKAGE_ID}` can resolve to an empty value for the same reason as in tracker
URLs.
Macros the seller's ad server fills, such as `{DOMAIN}`, `{APP_BUNDLE}`, and
`{PAGE_URL}`, are filled in your click-through URL when the shopper clicks. Ad
servers do not fill macros inside `rurl`, so Apostra also puts each one your
click-through URL uses on the click tracker as its own parameter (for example
`domain={DOMAIN}` or `purl={PAGE_URL}`), where the seller's ad server fills it.
Apostra's click redirect then copies that value into your landing page URL. A
macro is filled only when the seller provides its value; otherwise the token is
removed, so the shopper never sees placeholder text. `{PAGE_URL}` gives your
landing page the full address of the page where the ad ran. Apostra uses that
address only to build the redirect and stores only the page's host with the
click, because a full page address can contain personal data.
Serve-time environment macros such as `{CACHEBUSTER}`, `{DEVICE_TYPE}`,
`{COUNTRY}`, and consent strings remain literal in the outbound asset for the
publisher/ad server to fill when the impression serves. Self-contained HTML is
the exception: its embedded Apostra tracker fires directly in the browser, so
Apostra injects an HTML-safe URL with no unresolved identity placeholders. The
`tracking.supported_macros` array on the manifest response lists the macros the
format supports.
### Tracker templates
The manifest response returns the trackers as templates in
`tracking.impression_tracker_url_template` and
`tracking.click_tracker_url_template`. A template carries identity
placeholders, `{MEDIA_BUY_ID}` and `{PACKAGE_ID}`, and inside the click
tracker's `rurl` any `{CREATIVE_ID}` or `{CAMPAIGN_ID}` from your click-URL
parameters. Only Apostra can fill those ids: a seller's ad server has no value
for them, so a template copied into an ad server by hand records every
impression and click with placeholder text instead of ids, and none of them can
be attributed.
Do not traffic a template. When Apostra syncs the creative to a media buy, it
sends the seller trackers with these ids filled for that media buy, as described
above. Serve-time macros such as `{CACHEBUSTER}`, `{AXEM}` and `{TMPX}` are
correct in a template and in a synced tracker: the ad server fills them when
the impression serves.
`tracking.impression_tracker_url` and `tracking.click_tracker_url` are
deprecated aliases that return the same templates. They are removed on
9 November 2026; read the `_template` fields instead.
Only one clickthrough URL per manifest. If multiple URL assets are flagged `url_type: CLICKTHROUGH`, manifest creation fails.
## Measurement endpoint and EU consent
The injected impression and click trackers fire to `ping.apostra.com` —
Apostra's own measurement domain in production (`/agentic/imp` for impressions,
`/agentic/clk` for clicks). Because it is Apostra's domain rather than a
registered third party, Google Ad Manager doesn't recognize it as a known
ad-tech vendor (see below). It powers impression counting, frequency capping,
emissions measurement, and conversion attribution.
### What the tracker carries
The impression URL carries identifiers and tokens — never raw PII:
| Param | Meaning |
| - | - |
| `dsid` | Dataset ID — links the impression to the advertiser's conversion-attribution dataset |
| `cid` | Creative ID |
| `camp` | Campaign ID |
| `advid` | Advertiser ID |
| `mb` | Media buy ID, resolved when the creative is synced to the media buy |
| `pkg` | Package ID — left blank on synced creatives, which can map to multiple packages |
| `axem` | Emissions measurement token, filled at serve time |
| `tmpx` | HPKE-encrypted exposure token for frequency capping, filled at serve time (see [TMP](/v2/storefront/tmp/overview)) |
| `cb` | Cachebuster |
The click tracker carries the same identifiers and preserves the original
clickthrough in the `rurl` parameter.
`axem` and `tmpx` are filled by the **TMP Router** and are populated only when
the publisher has connected [TMP](/v2/storefront/tmp/overview) — without it they
resolve to empty, and the pixel still counts impressions but carries no
emissions or frequency-cap signal.
### Consent signals
Where the format or advertiser tracking config enables them, the tracker URL
includes consent macros as ad-server placeholders — `gdpr` (whether GDPR
applies), `gdpr_c` (the TCF consent string), and `us_p` (the US Privacy string).
The ad server fills them from the user's consent state at serve time and
forwards them to the endpoint. These are the only consent signals the pixel
carries today; there is no GPP (Global Privacy Platform) string.
### Clearing the Google Ad Manager "unknown ad technology" warning
When a creative is trafficked through Google Ad Manager, this clearing step is
the **publisher's** action (a GAM admin), not the buyer's. GAM scans the
creative for ad technology URLs and flags any domain it doesn't recognize on
Google's Ad Technology Providers list. Because `ping.apostra.com` is an Apostra
measurement domain, it surfaces under **Associated ad technology providers** as
an unknown URL with the note *"We found unknown URLs that may affect serving
eligibility."*
When the network has **Check reservation creatives for user consent** enabled,
an unrecognized domain blocks the creative from serving — including
non-personalized requests — in the EEA, the UK, and Switzerland; the Brazil
(LGPD) block applies regardless of that setting. The creative serves normally
everywhere else.
To clear it, the publisher adds `ping.apostra.com` as a custom ad partner URL
in the network's **EU user consent settings** (reachable from the **Go to
settings** link on the warning; the page requires specific GAM permissions, as
the warning notes). Selecting "No ad technology providers to declare" does *not*
clear a URL GAM has already detected in the creative.
## Format catalog
To inspect formats required by the campaign's selected products:
| Endpoint | Purpose |
| - | - |
| `GET /api/v2/buyer/campaigns/:campaignId/creatives/templates` | List the deduplicated V2 `format_options`, legacy format IDs, per-product requirements, and available templates for the campaign. |
| `GET /api/v2/buyer/formats` | Cross-agent legacy named-format registry (all legacy format IDs discovered from sales agents). |
| `GET /api/v2/buyer/formats/:agentUrl/:formatId` | Format details for a specific format on a specific agent. |
## Propagation to media buys
At campaign execute, the manifest is synced to each sales agent that supports its selected legacy or canonical format identity. Creating a complete manifest after the campaign already has non-draft media buys also propagates it to their active sellers, without a separate `sync_creatives` call. The same manifest can serve multiple media buys; later updates re-sync. Image and HTML5 creatives need a destination (`clickUrl` or a click-through/landing-page asset) to deliver. A video VAST creative needs either `clickUrl` or a non-empty `VideoClicks/ClickThrough` in its Inline VAST document; a Wrapper with a non-empty `VASTAdTagURI` delegates that destination to the downstream VAST. Use `sync_status` to confirm publishers have received the latest version. `synced` is true only when every current sales-agent link succeeded; `agent_count` counts successful agents, and `last_synced_at` comes only from successful links. A seller note appears only with that seller's rejected or changes-requested verdict for the current Creative revision. A local validation or delivery failure reads as Sync failed, never as seller feedback, and a later submission returns to Reviewing until the seller responds. The Creative Library detail shows the same links as a Seller approval table: Approved appears once the seller approves a delivered version, while Rejected and Changes requested remain only for the seller's current verdict. When a Creative has a click URL, its collapsed Destination section shows the authored URL, the delivered URL after effective additions, and whether each addition came from the advertiser, campaign, or Creative.
### Creative transport modes
Apostra chooses the creative transport mode per sales agent:
* **Creative library sync** — sellers that advertise a creative library through `creative.has_creative_library: true` receive manifests through `sync_creatives`, and packages reference them with `creative_assignments`.
* **Inline creative delivery** — sellers that advertise `creative.has_creative_library: false` receive the creative manifest inline on each package that needs it.
For legacy or unknown capabilities (`creative.has_creative_library` is `null` or absent), Apostra falls back to the seller's advertised tools and uses `sync_creatives` when available.
During initial media-buy execution, Apostra reuses creatives that have current-revision evidence in the same seller account's library. Those creatives remain `creative_assignments` on retries instead of being re-sent as inline creates. If `sync_creatives` fails for the remaining new creatives and the seller allows inline creatives, only those unsynced creatives fall back inline during execution; existing library creatives remain assignments. Media-buy updates retain the seller-wide inline fallback when sync fails. If the seller is library-only and a required new creative fails to sync, the create or update request fails synchronously instead of silently continuing.
## Related concepts
Owning campaign — formats come from its products
Brand info auto-resolved from the advertiser
# Dimensions and labels
Source: https://docs.apostra.com/v2/object-guides/dimension
Organize buyer objects and seller Library Material with dimensions and labels.
## Overview
A **dimension** is an account's named organizing axis, such as Market,
Quarter, or Brand. A **label** is one value from that dimension attached to an
advertiser, campaign, creative, or creative asset in a Buyer Account, or to
Library Material in a Seller Account. Dimensions make labels consistent across their applicable objects:
the same value means the same thing everywhere in the account.
Every Buyer Account also has the built-in `tags` dimension. It is open, so you
can apply a new tag value without creating it first. There is no separate tag
object or tag tool.
Dimensions organize work; they do not grant authority, select inventory, change
targeting, or cross an AdCP boundary.
Dimensions do not have their own screen. They appear as object labels, search
filters, buyer campaign-delivery report axes, and as one **Dimensions**
multi-select on the Advertisers and Campaigns Pages. Its values include the
dimension name, such as `Market › US`, and the control reads how many you have
selected. Each labeled row still shows its own labels as chips. The Pages are
for reading and filtering: open one campaign to edit its labels from the
Labels section when permitted. Ask your agent to create dimensions or to label
many campaigns or advertisers at once.
## Dimension fields
| Field | Type | Notes |
| - | - | - |
| `id` | string | Opaque server-issued identifier. Use it to read or update the dimension. |
| `key` | string | Immutable owner-chosen slug, unique in the account. It is the key in `labels` and `filter.labels`. |
| `name` | string | Display name. Rename this without changing `key`. |
| `valuesMode` | `open` or `governed` | Open dimensions accept a new normalized value when you apply a label. Governed dimensions accept only their listed active values. |
| `values` | array | Each value has a stable `value`, display `name`, and `retired` state. Retired values stay in history but cannot be newly applied. |
| `appliesTo` | array | Buyer dimensions may label any of `advertiser`, `campaign`, `creative`, and `creative_asset`. Seller dimensions may label only `material`. |
| `usage` | object | Read-only counts of labeled objects for each kind: advertisers, campaigns, creatives, creative assets, and Material. |
| `retired` | boolean | Retired dimensions cannot receive new labels but remain readable. |
## Create and manage dimensions
On the V3 MCP endpoint, use `save_dimension` to create a dimension with a
unique `key`, display `name`, value mode, applicable object kinds, and an
idempotency key. After creation, use its returned `id` for updates; `key` is
immutable. The same tool can add or rename values, merge a value into another
value, or retire a value or the whole dimension.
`mergeInto` must name an existing active target value. Merging moves existing
labels to that target but does not retire the source: retire a merged source
explicitly when it should no longer be available.
Buyer Accounts can have at most 20 dimensions; Seller Accounts
can have at most 20 seller-created dimensions. A dimension has at most 200
values, and an object has at most 50 labels. The built-in `tags` dimension
counts toward a Buyer's 20-dimension limit and always remains open. Seller
dimensions are available when the Library is available for your storefront and
can apply only to Material. Category, Topic, Audience, and Vertical are
system-owned, cap-exempt Material facets; sellers cannot create or edit them.
The daily seller Library seed reconciles those facets from Material metadata and
removes labels that metadata no longer supports.
In chat, ask to apply labels in bulk. The agent uses `save_dimension` and
`save_campaign` or `save_advertiser` as appropriate. Only the first-party host
re-reads an open Campaigns Page when the Page's data changes. Other chat clients do not
re-read, and Advertisers has no refresh path or advertiser label drill-in, so
advertiser label changes are made in chat.
## Built-in tags
Buyers can use the built-in `tags` dimension as one of their 20 account dimensions.
## Apply and find labels
Pass `labels` to `save_advertiser`, `save_campaign`, `save_creative`,
`upload_creative_asset`, or seller `save_material`:
```json theme={null}
{
"labels": {
"market": ["us", "canada"],
"tags": ["launch"]
}
}
```
Each supplied dimension replaces that dimension's labels on the object; pass
an empty array to clear that dimension. Omitting `labels` leaves labels
unchanged. `get` and `search` return the current labels as the same
`{ "dimensionKey": ["value"] }` shape.
Use `search` with `kind: "advertiser"`, `kind: "campaign"`,
`kind: "creative"`, `kind: "creative_asset"`, or seller `kind: "material"` and
`filter.labels`. The filter is AND across dimension
keys and OR within a value array. Use the literal `"unlabeled"` to find
objects with no active value for one dimension:
```json theme={null}
{
"kind": "campaign",
"filter": {
"labels": { "market": ["us", "canada"], "quarter": "unlabeled" }
}
}
```
List dimensions with `search({ "kind": "dimension" })`, optionally using
`filter.appliesTo`, then read one with `get({ "kind": "dimension", "id": "…" })`.
You cannot narrow `appliesTo` while the dimension still has labels on an object
kind being removed; clear or move those labels first.
## Group delivery by a label
For V3 buyer campaign delivery, pass a label axis as
`"labels."` in `get_delivery.dimensions`:
```json theme={null}
{
"report": "campaign_delivery",
"dimensions": ["labels.market"],
"range": { "startDate": "2026-08-01", "endDate": "2026-08-31" }
}
```
The report reads the current labels on both the campaign and its advertiser.
Rows with no active value are returned in the `"Unlabeled"` bucket. Because
labels are resolved when you query the report, a later relabeling reorganizes
historical rows too. An unknown, retired, or inapplicable key is rejected with
the account's available dimensions.
Label grouping applies to the stored `campaign_delivery` report. The additive
`live_campaign_delivery` mode preserves one exact campaign's provider response
and does not accept grouping dimensions.
## Related
Advertiser ownership and fields
Campaign ownership and fields
# Signal
Source: https://docs.apostra.com/v2/object-guides/signal
Audience and contextual segments registered, discovered, and deployed to DSPs
## Overview
A **Signal** is a named segment used for targeting — an audience, contextual cohort, or first-party data slice. Signals are registered with Apostra (or auto-discovered from external signals agents like LiveRamp), exposed via the v2 REST API, and deployed to DSPs/SSPs for use in media buys.
The v2 signal model is conceptually identical to v1 — the same registration, discovery, access tier, and deployment semantics apply — but is now exposed via stable v2 REST endpoints under `/api/v2/storefront/signals`.
**Storefront-mounted**: Signal management endpoints live on the storefront router (`/api/v2/storefront/signals/...`) even though buyers and advertiser operators interact with the resulting signals on campaigns. This is because signals are owned by the agent/advertiser (storefront context) and surfaced to buyers via discovery and campaign attachment.
## Signal sources
Audiences you upload — CRM segments, behavioral cohorts, geographic territories. No additional cost.
Surfaced by external signals agents (LiveRamp, Optable, custom) using the ADCP signals protocol. Agents own the segments they create.
External data providers (weather, behavioral, contextual). Premium signals incur additional CPM cost.
Apostra-provided quality and viewability signals always available — no setup required.
## Key fields
| Field | Type | Notes |
| - | - | - |
| `signalId` | string | Stable signal identifier. **You author it** — the platform does not assign one |
| `name` | string | Human-readable name |
| `description` | string | What the segment represents |
| `keyType` | string\[] | The identifiers the segment is keyed on. At least one required, and **immutable** after registration. See [Key types and the privacy boundary](#key-types-and-the-privacy-boundary) |
| `regions` | string\[] | Where the signal is available: `NORAM`, `LATAM`, `EMEA`, `APAC`, `ANZ`, `GLOBAL`. **Immutable** after registration |
| `isLive` | boolean | Deployed and accepting buys |
| `agentId` | string | Owning signals agent, on registration, for agent-managed signals |
| `access` | object\[] | Who may use the signal, and at what price — see [Access records](#access-records). Visibility and pricing live here, not on the signal |
Only `name`, `description`, `isLive` and the access records can change after
registration. `keyType`, `regions` and `metadata` are fixed — register a
replacement signal if the shape itself is wrong.
## Key types and the privacy boundary
`keyType` declares what the segment is keyed on, and every value belongs to
exactly one of two classes:
| Class | What it describes | Values |
| - | - | - |
| **Context** | The impression — where the ad runs | `country`, `region`, `metro`, `topic`, `eidr`, `gracenote`, `isrc`, `gtin`, `rss_guid`, `isbn`, `url`, `url_hash`, `custom` |
| **Identity** | The person | `maid`, `rampid`, `rampid_derived`, `hashed_email`, `hashed_phone`, `id5`, `uid2`, `euid`, `pairid` |
**A single signal cannot mix the two classes.** Registering one with both a
context and an identity key type is rejected: combining them would let the
reader correlate a person with the content they saw, which the AdCP
context/identity separation exists to prevent. Register two signals instead.
Use `custom` only when no structured type fits — it is the AdCP escape hatch,
where publisher and buyer agree out of band what the values mean.
## Lifecycle
Register first-party signals via `POST /api/v2/storefront/signals`. Discover external signals via `POST /api/v2/storefront/signals/discover` against signals agents.
Deploy signals to DSPs/SSPs so they become addressable in media buys. Agent-managed signals deploy through the agent.
Reference the signal as a target audience on a campaign (`audienceConfig.targetAudienceIds`) or in a brand story.
Track signal effectiveness through campaign reporting. Archive with `DELETE /api/v2/storefront/signals/:signalId`.
**Archiving cascades into targeting.** `DELETE` archives the signal, all of its
access records, **and every custom-signals targeting profile that references
it** — in either its include or its exclude set. Any buyer targeting built on
this signal stops working. Archiving is also one-way here: a repeat call reports
the signal as already archived rather than restoring it. Confirm nothing live
depends on the signal before you archive it.
## Common operations
### Register a first-party signal
```bash theme={null}
curl -X POST https://api.apostra.com/api/v2/storefront/signals \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"signalId": "high_value_customers",
"name": "High value customers",
"description": "Top 10% LTV customers — refreshed weekly",
"keyType": ["hashed_email"],
"regions": ["NORAM"],
"access": [
{ "advertiserId": 88421, "visibility": "PROPRIETARY" }
]
}'
```
### Discover signals from an agent
```bash theme={null}
curl -X POST https://api.apostra.com/api/v2/storefront/signals/discover \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agentId": "liveramp_signals_agent",
"signalSpec": "premium auto intenders, last 30 days"
}'
```
Discovery reads the connected agents' catalogs live and **persists nothing** —
it returns what is on offer. Registering something it found is a separate
`POST /signals` call. It only returns results for signals agents already
connected to your account, so an empty response means "no connected agent
matched", not "no such segment exists".
The discover endpoint queries the agent's catalog (RAG-style) and returns the most relevant segments along with metadata.
### List / get / update / delete
```bash theme={null}
# List with filters
curl "https://api.apostra.com/api/v2/storefront/signals?agentId=liveramp_signals_agent&visibility=PROPRIETARY&isLive=true&limit=100" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
# Filter to a specific advertiser
curl "https://api.apostra.com/api/v2/storefront/signals?advertiserId=88421" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
# Get one
curl https://api.apostra.com/api/v2/storefront/signals/sig_abcd \
-H "Authorization: Bearer $SCOPE3_API_KEY"
# Update
curl -X PUT https://api.apostra.com/api/v2/storefront/signals/sig_abcd \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "description": "Refined definition — top 5% LTV" }'
# Archive
curl -X DELETE https://api.apostra.com/api/v2/storefront/signals/sig_abcd \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
## Access records
Visibility and price are **not fields on the signal**. They live on `access`
records, one per advertiser the signal is offered to, so the same signal can be
public to one buyer and priced for another.
| Field | Type | Notes |
| - | - | - |
| `advertiserId` | int64 | Apostra advertiser this record grants access to |
| `visibility` | enum | `PUBLIC` or `PROPRIETARY` |
| `price` | object | Optional per-advertiser pricing option |
This access-record price is separate from pricing published by a managed sales
agent. For managed signal catalogs, each `custom` pricing option must include a
non-empty `metadata` object describing the charge basis. See [Manage signals
with your sales agent](/v2/storefront/esa/signals#signal-pricing).
| Visibility | Description |
| - | - |
| `PUBLIC` | Discoverable platform-wide |
| `PROPRIETARY` | Scoped to the advertiser named on the record |
Update access with `addAccess`, `updateAccess` (by `accessId`) and
`archiveAccess` on `PUT /signals/:signalId`. Archiving the signal archives every
one of its access records too.
Partner agents can list and manage signals across multiple advertisers they have access to. The `advertiserId` filter on list lets a partner narrow to one client at a time.
## Signal RAG and optimization
When a campaign runs, Apostra's RL optimizer uses signals as inputs alongside built-in quality and viewability data:
1. **Collect** — gather available signals on each impression opportunity (custom, third-party, agent-managed, built-in)
2. **Retrieve** — pull signals relevant to the campaign's brief and optimization goals
3. **Optimize** — bias bidding and budget allocation toward signal combinations that perform well
4. **Learn** — feed measured outcomes back to inform future signal usage
This is the model AdCP and Apostra refer to as **Signal RAG**. The platform documents which signals drove decisions so you can trace optimization choices.
## Deployment
Some signals require deployment to a DSP before they're addressable:
* **First-party**: deployed by Apostra to integrated DSPs after registration
* **Agent-managed**: deployed by the agent (e.g. LiveRamp deploys RampIDs to its connected DSPs)
* **Built-in**: always available, no deployment needed
`isLive: true` in the response confirms the signal is deployed and bidding is possible.
## Pricing
| Source | CPM premium |
| - | - |
| First-party | None |
| Agent-managed | Per-agent (LiveRamp, Optable, etc.) |
| Third-party | Per-provider; surfaced in pricing fields |
| Built-in | None |
Premium signal cost is added on top of the publisher's CPM at the media buy level and is paid out of the media portion of your gross budget — see the media buy's `budget_breakdown` and [Budgets and fees](/v2/concepts/budgets-and-fees) for how the media/fee split is reported.
## Related concepts
Reference signals as `audienceConfig.targetAudienceIds`
Brand identity that contextualizes signal usage
Discovery uses signals to refine product matches
Browse storefronts and register credentials per inventory source
# Storefront
Source: https://docs.apostra.com/v2/object-guides/storefront
Browse storefronts, authorize seller access, and manage provider accounts
## Overview
A **Storefront** is a publisher's buyer-facing home on Apostra: the business presence, name, description, and discovery surface buyers use to understand who they are buying from. The publisher's **Merchandising Agent** runs that Storefront. It implements the AdCP media-buy workflow, responds to buyer briefs, and draws from one or more **inventory sources**.
At signup, **Just list** creates the Storefront and lets the seller connect and
manage inventory with an agent operated by the seller, a partner, or another
provider. That agent can be connected during setup and does not need to exist
at signup. **Agentic Media Company** includes listing and adds Apostra's hosted
Merchandising Agent, which the seller trains for its business. This mutable
product choice is separate from the implementation of any inventory source,
which can be connected later through its supported setup path.
In the buyer V3 object model, this Storefront identity is projected as a
**seller**. The seller exists independently of buyer authorization. A
**connection** is one authorization grant from the buyer account to that
seller; one seller can have zero, one, or several connections. Each connection
can discover several provider accounts, which can then be mapped to
advertisers. See [Media Partners and connections](/v2/buyer/storefronts/overview) for
the buyer workflow and noun boundary.
Each connected provider account also has its own **Enhanced Reporting** control.
It records a reporting preference for that exact account; the account's existing
reporting subscription starts the reporting/history work and can retain up to
one year of history. A charge is earned only after that subscription completes a
successful sync after the control is enabled. Disabling the control stops future
enhanced work without deleting history already collected. It is available in
Connections and through the connection's `save_connection` MCP operation. An
enabled account without a reporting subscription or successful sync is not
billed; an account without billing authority cannot be enabled.
Seller selection has two explicit grains. **Always include** and **Always
exclude** are buyer-account controls for the Storefront. When the account uses
**Default**, each advertiser can separately choose **Inherit**, **On**, or
**Off**; Inherit follows automatic market/channel matching. These preferences
control future activation and buying without deleting shared authorization
grants, provider accounts, mappings, or history.
Inventory sources can be external sales agents you already run, your own ad servers with Apostra-managed sales-agent plumbing behind them, or other Storefronts that have authorized passthrough/federation.
Buying through a source-backed storefront is gated on **credentials per source**: every inventory source declares whether it requires authentication, and the buyer registers credentials (API key, JWT, or OAuth) once per source. Once a source reports `connected: true`, discovery and media buys flow through it normally. Adapter storefronts use authorization connections instead: authorize or reconnect the seller connection, then map one of its discovered provider accounts to an advertiser.
```
Storefront
├── Inventory source(s) (seller-side supply)
│ └── V2 source credential(s) (per source; not V3 grants)
└── Buyer V3 connection(s) (0..N authorization grants)
├── Grant credential
└── Discovered provider account(s)
└── Advertiser mapping(s)
```
This page covers the **buyer** side: browsing storefronts, expressing interest
in upcoming supply, and wiring up credentials. If you operate a storefront,
see [Storefront onboarding](/v2/setup/storefront-onboarding).
All examples below use the buyer base URL:
```
https://api.apostra.com/api/v2/buyer
```
## Why storefronts matter
* **One-stop access to publisher inventory** — a storefront aggregates multiple inventory sources behind a single ID, so buyers don't track agent endpoints individually
* **Per-source credential management** — credentials are scoped to the inventory source, so rotating or revoking one doesn't affect access to the others
* **AAO registry signals** — Apostra reads [Agentic Advertising Organization](https://agenticadvertising.org) registration and compliance signals; registration gates external source connection, compliance is advisory for activation, and Apostra marketplace review controls public listing
* **Multiple auth options** — sources support API key, JWT, and OAuth flows, so the platform fits whatever the publisher's agent already uses
* **Discoverable upcoming supply** — `/storefronts/discover` and `/storefronts/interest` let buyers see and request access to publishers they aren't yet connected to
## Adapter credential health
Connected adapter credentials continue through a health loop after setup. OAuth credentials refresh lazily during use and proactively in a nightly sweep. If refresh fails because the token is expired or revoked, the adapter credential moves to `EXPIRED`. If a delegated adapter call returns an auth or permission failure that cannot be fixed by retrying, the adapter credential moves to `EXPIRED` or `ERROR` and the connection summary reports `error`.
API-key and bearer adapter credentials are not refreshable. When they carry an expiry timestamp, the sweep notifies the operator before expiry and marks the adapter credential `EXPIRED` after expiry. Reconnect or rotate the adapter credential to restore the storefront connection.
Adapter credential recovery is not a generic API-call fallback. Use the typed storefront adapter connection flow to reconnect OAuth accounts or rotate provider tokens, then continue with typed adapter operations such as listing and linking advertiser accounts.
## Storefronts in buyer discovery
Every Storefront with canonical `canTransact: true` and an approved marketplace listing is exposed through its Merchandising Agent for public buyer-side product discovery. Clearing the seller's intake hold does not bypass readiness or marketplace review. The generated sales agent ID is `storefront-{platform_id}`, where `platform_id` is the Storefront's public `platformId` slug; for example, a Storefront with `platformId: "premium-ctv"` appears as `salesAgentId: "storefront-premium-ctv"` in discovery results. Existing Storefronts are registered automatically once listed, so buyers can use the standard discovery flow without learning a separate endpoint.
The Merchandising Agent is the buyer-facing ADCP actor for the Storefront, not a replacement for inventory sources, source IDs, or underlying source agent IDs. Composition Storefronts return products assembled from active ingredient sources and active product rules. Passthrough Storefronts proxy `get_products` to an active upstream inventory source and apply Storefront identity, metadata, and buyer-instruction overlays to the response. Buyer instructions are resolved from operator domain, brand domain, and optional country.
New Storefronts start in a pending-review marketplace state. They can be
configured and used by the owning organization, and they can open for known
transactions, but public buyer discovery only includes Storefronts that Apostra
has reviewed and listed. Admins can also hide a listed Storefront, which removes
it from public buyer discovery without deleting the Storefront or its inventory
sources.
## Key fields
The storefront resource has two response shapes:
* **Summary** — returned by `GET /storefronts` (list). Row-level fields plus scalar source counts.
* **Full** — returned by `GET /storefronts/:id` (get). The complete storefront resource with rolled-up connection state and buyer accounts. Source-level diagnostic rows are returned by `GET /storefronts/:id/capabilities`.
### Storefront brand identity
An operator Storefront can persist `brandName`, `logoUrl`, and
`logoBackground` for its `operatorDomain`. These fields are projections of
owner-asserted identity from the AAO registry and the domain's `brand.json`;
Apostra does not fill them from Brandfetch or website inference.
The Listing page identifies whether that identity is
self-hosted, AAO-managed, a community profile, or missing. A self-hosted
`brand.json` is read-only in Apostra and must be changed on the operator's
website; AAO-managed identities are changed through the connected AAO account.
A missing reusable logo does not prevent a channel from accepting its own
direct asset uploads.
Official adapter Storefronts are the exception to the display-name source:
their listing uses the represented platform's registered adapter name
(for example, Meta or TikTok), even when Apostra operates the adapter. The
operator account remains separate provenance and does not replace the platform
identity buyers see.
`POST /api/v2/storefront/resolve-brand` performs a fresh authoritative lookup.
When the registry confirms that no identity exists, it returns
`resolved: false` without a resolved logo. Positive owner-asserted fields are
reconciled back to the same brand domain. A missing record or logo does not
erase an existing field because legacy rows do not record whether that field
was inferred or explicitly supplied; absence is not proof that an explicit
value should be deleted. If the registry cannot complete the fresh origin
check, the endpoint returns `503 SERVICE_UNAVAILABLE` and leaves the current
identity unchanged. Identity reconciliation is conditional on the operator
domain still matching the domain that was resolved, so a concurrent domain
change cannot receive older identity data.
See [Resolve brand](/v2/storefront/tasks/resolve-brand) for the request and
response contract.
### Modular inventory source readiness
Operator-owned storefronts can include `MODULAR_SOURCE` inventory sources. A
modular source is composed from smaller source-side modules: inventory-feed
ingestion, a booking ledger, execution handoff, creative mapping, status sync,
reporting import, and source-side human work queues.
The modular source runtime projection includes `lifecycleSummary`, which is the
operator-facing rollup for each stage:
| Status | Meaning |
| - | - |
| `UNSUPPORTED` | Every attached module declared what it covers, and none of them implements the stage. Attach a module of the kind that runs it. |
| `NOT_DECLARED` | A module that could run the stage is attached, but we could not read what it does — whether it runs the stage is unknown, not absent. The stage's `issues` name that module and `modes` is empty. Do not read the stage as a setup gap; nothing in a seller's configuration produces or clears this state. |
| `MISSING_SETUP` | A module supports the stage, but required module config is missing. |
| `RUNTIME_INPUTS_REQUIRED` | Setup is complete, but the stage needs runtime data such as avails, creative refs, media-buy fields, or reporting uploads. |
| `HITL_PENDING` | Setup is complete and at least one open human work item is waiting for this stage. |
| `READY` | The stage has the setup and runtime inputs it needs for the current source state. |
| `BLOCKED` | A module has a source-health issue or error that needs operator attention. |
`activeAvailCount` is the number of active normalized avails available to the
source projection. `openWorkItemCountsByKind` groups source-side queue items by
kind, such as `CONFIRM_AVAILS`, `TRAFFIC_TO_CADENT`, `MAP_CREATIVE_REF`, or
`UPLOAD_FINAL_REPORT`.
Use the detailed [modular inventory source lifecycle guide](/v2/storefront/inventory-sources/modular-lifecycle)
for the operator task sequence: preview and commit avails, inspect products,
reserve capacity, prepare execution handoff, release capacity, and work HITL
queue items.
### Creative library capability
Storefront and source capability caches store AdCP `creative.has_creative_library` as `creativeHasCreativeLibrary`. Apostra uses this tri-state field to choose creative transport for media buys:
| State | Meaning | Creative transport |
| - | - | - |
| `true` | Seller has a creative library. | `sync_creatives` is required and packages reference synced manifests with `creative_assignments`. |
| `false` | Seller does not have a creative library. | Creative manifests are delivered inline on package payloads. |
| `null` or absent | Unknown, legacy, or synthetic capability metadata. | Apostra falls back to the advertised tool list and uses `sync_creatives` when available. |
### Operator `businessProfile`
Operator setup surfaces may include a `businessProfile` object captured during
Murph-led storefront setup. This profile helps the Merchandising Agent describe
the operator's business, follow the right merchandising guidance, and
understand which publisher domains it should be authorized to sell through the
Storefront.
| Field | Type | Description |
| - | - | - |
| `agentName` | string | Deprecated agent-name field, kept for legacy compatibility only. Prefer the Storefront display name for buyer-facing naming. Maximum 80 characters. |
| `agentPersonality` | string | **Deprecated.** An agent has no personality of its own — voice belongs to the brand and is read from the brand manifest (`tone`). Nothing consumes this field. Maximum 1000 characters. |
| `publisherDomains` | string\[] | Publisher domains or properties the Merchandising Agent should be authorized to sell through the Storefront. Up to 128 valid domains; each domain is normalized to lowercase, deduplicated, and limited to 253 characters. |
```json Example theme={null}
{
"businessProfile": {
"publisherDomains": ["touchline.example", "football.touchline.example"]
}
}
```
Omitting `publisherDomains` means the authorized domains are unknown or have
not been captured yet. It does not mean the Merchandising Agent is authorized
to sell every domain through the Storefront.
For managed sales-agent storefronts, this field is the authoritative
publisher-domain set Apostra syncs to the managed sales agent. See
[Publisher domains](/v2/storefront/publisher-domains) for replacement and
removal behavior.
### Murph `sellerAnalytics`
When a storefront operator asks Murph for seller analytics, Murph can attach a
`sellerAnalytics` payload to the chat response for visual MCP App rendering.
The payload includes discovery-run mix, buyer asks, top surfaced products, and
commercial outcomes attributed to those runs.
The `historicalPerformance` object summarizes the same window into the
negotiation signals Murph and the Merchandising Agent can use:
| Field | Type | Description |
| - | - | - |
| `runCount` | integer | Recent intelligence runs considered |
| `bookedRunCount` | integer | Runs with at least one attributed booked media buy |
| `winRate` | number | Share of recent runs that booked, from `0` to `1` |
| `askRunCount` | integer | Runs with buyer asks, price objections, or packaging asks |
| `askToBookConversionRate` | number \| null | Share of ask runs that booked, or `null` when no ask runs exist |
| `priceObjectionConversionRate` | number \| null | Share of price-objection runs that booked, or `null` when none exist |
| `packagingAskConversionRate` | number \| null | Share of packaging-ask runs that booked, or `null` when none exist |
| `averageBookedBudget` | number \| null | Average booked budget per attributed booked media buy |
| `averageProductsPerRun` | number | Average products shown per run |
| `averageProductsPerBookedRun` | number \| null | Average products shown on booked runs |
| `averageProductsPerUnbookedRun` | number \| null | Average products shown on unbooked runs |
| `deliveryRate` | number \| null | Delivery-report events divided by booked media buys, or `null` when none booked |
| `repeatBuyerCount` | integer | Buyer identities with at least two runs in the window |
| `recommendedPosture` | string \| null | Directional negotiation posture: `hold_value`, `value_preserving_compromise`, `tradeoff_ladder`, `direct_fit`, `price_first`, or `wholesale_mirror` |
The `strategySignals[]` array turns `historicalPerformance` into deterministic
strategy guidance for sellers:
| Field | Type | Description |
| - | - | - |
| `id` | string | Stable signal key within the generated analytics payload |
| `priority` | `"high"` \| `"medium"` \| `"low"` | Suggested attention level |
| `label` | string | Short display label |
| `detail` | string | Human-readable reason for the signal |
| `posture` | string | Suggested negotiation posture |
| `evidence` | array | Small list of `{ label, value }` metrics backing the signal |
The `postureConversion.adherence` object reports how often the negotiation
posture the agent actually used matched the history-derived recommendation, and
how win rate differs when the seller follows that recommendation versus diverges
from it:
| Field | Type | Description |
| - | - | - |
| `comparableRunCount` | integer | Runs where a recommended posture existed to compare the chosen posture against |
| `followedCount` | integer | Comparable runs where the chosen posture matched the recommendation |
| `followedWinRate` | number \| null | Share of followed runs that booked, from `0` to `1`, or `null` when no run had a recommendation to compare against |
| `divergedWinRate` | number \| null | Share of diverged runs that booked, from `0` to `1`, or `null` when no run had a recommendation to compare against |
When the Merchandising Agent composes products, the same recent outcome history can tune its
built-in negotiation defaults, including posture selection, target product
count, pricing guidance, and packaging guidance. Human operating instructions
and buyer-specific instruction notes remain authoritative over these historical
signals.
The top-level `outcomes` object summarizes attributed outcomes across the
window:
| Field | Type | Description |
| - | - | - |
| `attributedRunCount` | integer | Number of recent runs with at least one attributed outcome event |
| `eventCount` | integer | Total attributed outcome events |
| `submittedCount` | integer | Media buys submitted for seller approval |
| `forwardedCount` | integer | Media buys forwarded to inventory sources |
| `forwardFailedCount` | integer | Media-buy forward attempts that failed |
| `rejectedCount` | integer | Media buys rejected by the seller |
| `deliveryReportedCount` | integer | Delivery report events attributed to recent runs |
| `bookedMediaBuyCount` | integer | Unique attributed media buys with a booked budget |
| `bookedBudget` | number | Sum of attributed booked budget in the analytics window, counted once per media buy |
| `deliveredImpressions` | integer | Sum of attributed delivered impressions |
| `deliveredSpend` | number | Sum of attributed delivered spend |
| `deliveredCurrency` | string \| null | Delivery spend currency, or `null` when no delivery currency is known or currencies are mixed |
Each item in `runs[]` includes an `outcome` object for that run:
| Field | Type | Description |
| - | - | - |
| `eventCount` | integer | Attributed outcome events for the run |
| `latestType` | string \| null | Latest attributed event type, or `null` when no events are attributed |
| `latestStatus` | string \| null | Latest media-buy status, or `null` when no status is available |
| `mediaBuyId` | string \| null | Latest attributed media-buy ID, or `null` when no media buy is attributed |
| `submittedCount` | integer | Media buys submitted for seller approval |
| `forwardedCount` | integer | Media buys forwarded to inventory sources |
| `forwardFailedCount` | integer | Media-buy forward attempts that failed |
| `rejectedCount` | integer | Media buys rejected by the seller |
| `deliveryReportedCount` | integer | Delivery report events attributed to the run |
| `bookedMediaBuyCount` | integer | Unique attributed media buys with a booked budget |
| `bookedBudget` | number \| null | Sum of attributed booked budgets for the run, counted once per media buy, or `null` when no budget is available |
| `deliveredImpressions` | integer | Delivered impressions attributed to the run |
| `deliveredSpend` | number | Delivered spend attributed to the run |
| `deliveredCurrency` | string \| null | Delivery spend currency, or `null` when no delivery currency is known or currencies are mixed |
`latestType` is one of `media_buy_submitted_for_approval`,
`media_buy_forwarded`, `media_buy_forward_failed`, `media_buy_rejected`, or
`delivery_reported`.
The `buyers[]` array rolls the same window up by buyer identity:
| Field | Type | Description |
| - | - | - |
| `operatorDomain` | string \| null | Buyer operator domain when known |
| `brandDomain` | string \| null | Buyer brand domain when known |
| `country` | string \| null | Buyer country when known |
| `firstRunAt` | string \| null | Earliest run timestamp for this buyer in the analytics window |
| `lastRunAt` | string \| null | Latest run timestamp for this buyer in the analytics window |
| `runCount` | integer | Recent intelligence runs for this buyer identity |
| `bookedRunCount` | integer | Runs for this buyer with at least one attributed booked media buy |
| `winRate` | number | Share of this buyer's recent runs that booked, from `0` to `1` |
| `askRunCount` | integer | Runs for this buyer with buyer asks, price objections, or packaging asks |
| `askToBookConversionRate` | number \| null | Share of this buyer's ask runs that booked, or `null` when no ask runs exist |
| `attributedRunCount` | integer | Runs with at least one attributed outcome event |
| `eventCount` | integer | Attributed outcome events for this buyer identity |
| `totalShownProducts` | integer | Products shown across the buyer's runs |
| `averageShownProducts` | number | Average products shown per run |
| `priceObjectionCount` | integer | Runs with detected price objections |
| `packagingAskCount` | integer | Runs with detected packaging asks |
| `requestAskCount` | integer | Buyer request-level asks detected in the window |
| `productAskCount` | integer | Product-level asks detected in the window |
| `proposalAskCount` | integer | Proposal-level asks detected in the window |
| `bookedMediaBuyCount` | integer | Unique attributed media buys with a booked budget |
| `bookedBudget` | number | Sum of attributed booked budget for this buyer identity |
| `averageBookedBudget` | number \| null | Average booked budget for this buyer's attributed booked media buys |
| `deliveredImpressions` | integer | Delivered impressions attributed to this buyer identity |
| `deliveredSpend` | number | Delivered spend attributed to this buyer identity |
| `deliveredCurrency` | string \| null | Delivery spend currency, or `null` when no delivery currency is known or currencies are mixed |
| `recommendedPosture` | string \| null | Buyer-specific directional negotiation posture |
The `seasonality[]` array buckets the same analytics window by UTC month:
| Field | Type | Description |
| - | - | - |
| `period` | string | Month bucket in `YYYY-MM` form |
| `runCount` | integer | Intelligence runs in the period |
| `bookedRunCount` | integer | Runs in the period with at least one attributed booked media buy |
| `winRate` | number | Share of period runs that booked, from `0` to `1` |
| `askRunCount` | integer | Period runs with buyer asks, price objections, or packaging asks |
| `askToBookConversionRate` | number \| null | Share of period ask runs that booked, or `null` when no ask runs exist |
| `priceObjectionCount` | integer | Period runs with detected price objections |
| `packagingAskCount` | integer | Period runs with detected packaging asks |
| `bookedBudget` | number | Sum of attributed booked budget in the period |
| `averageBookedBudget` | number \| null | Average booked budget across booked runs in the period |
| `averageShownProducts` | number | Average products shown per run in the period |
| `recommendedPosture` | string \| null | Directional posture inferred for the period |
The `sellerRecommendations[]` array turns those directional signals into
seller actions:
| Field | Type | Description |
| - | - | - |
| `id` | string | Stable recommendation key within the generated analytics payload |
| `priority` | `"high"` \| `"medium"` \| `"low"` | Suggested attention level |
| `kind` | `"protect_expand"` \| `"prioritize_follow_up"` \| `"packaging_friction"` \| `"price_resistance"` \| `"tighten_selection"` | Recommendation category |
| `title` | string | Short display title |
| `detail` | string | Human-readable reason for the recommendation |
| `action` | string | Suggested next seller action |
| `buyer` | object \| null | Buyer identity the recommendation is about, or `null` for global seller guidance |
| `evidence` | array | Small list of `{ label, value }` metrics backing the recommendation |
Seller analytics outcome totals use last-touch product-overlap attribution and
are directional. Seller recommendations are deterministic coaching signals
derived from the same window, not automated pricing or packaging changes. Use
buyer reporting endpoints for audited campaign delivery reporting.
### `BuyerStorefrontSummary` (list rows)
| Field | Type | Description |
| - | - | - |
| `id` | integer | Storefront ID |
| `platformId` | string | Public-facing slug |
| `name` | string | Storefront display name |
| `publisherDomain` | string \| null | Publisher domain |
| `sourceCount` | integer | Total number of inventory sources in this storefront |
| `connectedSourceCount` | integer | Number of sources the buyer is already wired to use |
Use `GET /storefronts/:id` when you need rolled-up credential state and buyer accounts. Use `GET /storefronts/:id/capabilities` when you need source-level diagnostic rows.
### `BuyerStorefront` (single storefront)
| Field | Type | Description |
| - | - | - |
| `id` | integer | Storefront ID |
| `platformId` | string | Public-facing slug |
| `name` | string | Storefront display name |
| `publisherDomain` | string \| null | Publisher domain |
| `connected` | boolean | Whether the buyer has the required credentials/account setup to use the storefront |
| `requiresCredentials` | boolean | Whether any source behind the storefront requires buyer credentials |
| `customerAccounts` | `[{ accountIdentifier, status }]` | Buyer's registered accounts, deduped across the storefront |
| `adcpCapabilities` | object \| null | Rolled-up capability manifest when cached capability data exists |
### Storefront capability source rows
| Field | Type | Description |
| - | - | - |
| `sourceId` | string | Inventory source identifier within the storefront |
| `sourceName` | string \| null | Inventory source display name |
| `executionType` | string | Source execution type |
| `agentName` | string \| null | External sales-agent name when one exists |
| `requiresCredentials` | boolean \| null | Whether buyer-provided credentials are required for this external AdCP source. `null` when not applicable or unavailable |
| `probeable` | boolean | Whether this row is an external AdCP sales agent that the capability diagnostic can check |
| `probeStatus` | string | `reachable`, `unreachable`, `not_applicable`, or `unavailable`. `reachable` means capabilities are available (including synthesized capabilities for adapter and ESA storefronts), possibly from cache |
| `message` | string \| null | Explanation when the row is not probeable |
| `capabilities` | object \| null | Capability summary for external AdCP sources; `null` for managed sources or failed capability lookups |
Managed ad-server-backed sources appear in the capability response with
`probeable: false` and `probeStatus: "not_applicable"`. That means they are not
external AdCP agents probed by this endpoint; it does not mean the source is
unreachable.
### Storefront readiness diagnostics
Storefront operators can call `GET /api/v2/storefront/readiness` to inspect
setup progress. In addition to the top-level readiness checks, the response
includes `sourceDiagnostics[]`, a per-inventory-source diagnostic summary used
by Murph and the storefront setup rail.
Each source diagnostic includes:
| Field | Description |
| - | - |
| `id`, `sourceId`, `name` | Internal row id, storefront source id, and display name. |
| `executionType`, `sourceStatus` | Source type and current storefront source status. |
| `capabilities` | Support state for `products`, `createMediaBuy`, `updateMediaBuy`, `signals`, and `wholesaleProducts`. Synthetic or missing manifests report unknown capabilities rather than hard unsupported states. |
| `auth` | Whether source authentication is required, configured, and which auth type is expected. |
| `productBuilder` | Whether the source is used for product composition, Agent-supplied routing, or is still unknown, plus wholesale product and signal counts when available. |
| `compliance` | Latest AAO compliance summary, including pass/fail state, checked timestamp, and track counts. |
| `debug` | Sanitized capability/debug metadata such as advertised tools, channels, publisher domains, and synthetic-manifest state. |
| `lastActivity` | Latest setup activity for the inventory source, when available. |
## Common operations
### Listing storefronts
`GET /storefronts` returns the storefronts you can transact with, paginated.
```bash List theme={null}
curl "https://api.apostra.com/api/v2/buyer/storefronts?limit=20" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```bash Filter by name theme={null}
curl "https://api.apostra.com/api/v2/buyer/storefronts?name=premium&limit=20" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Query parameters:
| Param | Notes |
| - | - |
| `name` | Case-insensitive partial match on storefront name |
| `limit` | 1..50 (default 20) |
| `offset` | Pagination offset (default 0) |
Response (summary rows):
```json theme={null}
{
"items": [
{
"id": 42,
"platformId": "premium-ctv",
"name": "Premium CTV Storefront",
"publisherDomain": "premium-ctv.example.com",
"sourceCount": 3,
"connectedSourceCount": 1
}
],
"total": 1,
"hasMore": false,
"nextOffset": null
}
```
For rolled-up credential state and buyer accounts, call `GET /storefronts/:id`.
When `hasMore` is `true`, pass `nextOffset` back as the `offset` parameter to
walk pages.
### Getting a storefront
`GET /storefronts/:id` returns the full `BuyerStorefront` resource, including
rolled-up `connected`, `requiresCredentials`, and `customerAccounts`. Use this
when you need fresh storefront connection state (e.g. after registering
credentials or completing the OAuth flow) or when a row-level summary isn't
enough.
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/storefronts/42" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```json Response theme={null}
{
"id": 42,
"platformId": "premium-ctv",
"name": "Premium CTV Storefront",
"publisherDomain": "premium-ctv.example.com",
"connected": false,
"requiresCredentials": true,
"customerAccounts": [
{
"accountIdentifier": "buyer-seat-123",
"status": "ACTIVE"
}
],
"adcpCapabilities": null
}
```
### Inspecting source capabilities
`GET /storefronts/:id/capabilities` returns active source rows for capability
diagnostics. External AdCP sales-agent sources are capability-checkable through
the platform's cached-or-refreshed capability lookup. Managed ad-server-backed
sources are included but marked `probeable: false`.
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/storefronts/42/capabilities" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```json Response theme={null}
{
"storefrontId": 42,
"agents": [
{
"sourceId": "src_main",
"sourceName": "Premium CTV Direct",
"executionType": "AGENT",
"agentName": "Premium CTV Sales Agent",
"requiresCredentials": true,
"probeable": true,
"probeStatus": "reachable",
"message": null,
"capabilities": {
"version": "3.1.0",
"tools": ["get_products", "create_media_buy"],
"protocols": ["mcp"],
"sandboxSupported": true,
"synthetic": false
}
}
]
}
```
### Discovery (lightweight)
`GET /storefronts/discover` returns every marketplace-listed storefront visible
to a buyer - the live ones (`TRANSACTING`) and any listed storefront still
working through the pre-launch lifecycle (`UNCLAIMED`, `VERIFICATION`,
`CONFIGURING`, `TESTING`) - with just `id`, `name`, `publisherDomain`, and
`status`. This is the right endpoint for buyers who don't yet have programmatic
access but want to see what supply is coming.
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/storefronts/discover" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```json theme={null}
{
"items": [
{
"id": 42,
"name": "Premium CTV Storefront",
"publisherDomain": "premium-ctv.example.com",
"status": "TRANSACTING"
},
{
"id": 77,
"name": "Travel Vertical Network",
"publisherDomain": null,
"status": "CONFIGURING"
}
]
}
```
Discovery is intentionally minimal — no source list, no credential state.
Use it to populate a "what's available" picker, then submit interest with
the IDs you care about.
### Submitting interest
For private or upcoming storefronts you don't yet have access to, submit an
interest form. Apostra's onboarding team picks it up via Slack.
`POST /storefronts/interest` accepts:
| Field | Type | Notes |
| - | - | - |
| `storefrontIds` | `number[]` | Up to 100 storefront IDs you're interested in |
| `notes` | string | Free-form notes, up to 4000 chars |
You must provide at least one of `storefrontIds` or `notes`.
```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/buyer/storefronts/interest" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"storefrontIds": [42, 77],
"notes": "Targeting Q3 CTV upfronts; need access to travel vertical."
}'
```
```json theme={null}
{
"submitted": true,
"submittedAt": "2026-04-26T15:00:00.000Z"
}
```
To check whether you've already submitted, call `GET /storefronts/interest`:
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/storefronts/interest" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```json theme={null}
{
"submitted": true,
"submittedAt": "2026-04-26T15:00:00.000Z"
}
```
### Listing your credentials
`GET /storefronts/credentials` returns every credential you've registered,
scoped to the storefront sources each credential gives access to. Use it to
build a single "connected sources" view in your dashboard without paging
through each storefront.
```bash theme={null}
curl "https://api.apostra.com/api/v2/buyer/storefronts/credentials" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
```json Response theme={null}
[
{
"id": "722",
"accountIdentifier": "your-account-id",
"accountType": "CLIENT",
"status": "ACTIVE",
"registeredBy": "user@example.com",
"createdAt": "2026-02-23T19:55:11.602Z",
"updatedAt": "2026-02-23T19:56:56.272Z",
"sources": [
{
"storefrontId": 42,
"storefrontName": "Premium CTV Storefront",
"sourceId": "src_main",
"sourceName": "Premium CTV — Direct"
}
]
}
]
```
Each credential lists every `(storefrontId, sourceId)` pair it covers in
`sources[]`. A single credential can cover the same source across multiple
storefronts when the underlying agent is shared.
### Registering credentials per source
When a storefront reports `requiresCredentials: true`, at least one source
behind it needs buyer credentials before discovery or media buys can flow.
Use the capabilities endpoint to identify active source IDs and inspect
top-level `requiresCredentials` on external AdCP sources. Use the credentials
list to see which `(storefrontId, sourceId)` pairs already have credentials,
then register credentials for the external source that still needs them. If
`requiresCredentials` is `null`, do not infer that registration is required from
the capability row alone.
`POST /storefronts/:storefrontId/sources/:sourceId/credentials`
Check the parent storefront first. If `requiresCredentials` is `false`, the
buyer does not need this credential step for that storefront.
```bash API key theme={null}
curl -X POST \
"https://api.apostra.com/api/v2/buyer/storefronts/42/sources/src_main/credentials" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountIdentifier": "your-account-id",
"auth": {
"type": "api_key",
"token": ""
}
}'
```
```bash JWT theme={null}
curl -X POST \
"https://api.apostra.com/api/v2/buyer/storefronts/42/sources/src_main/credentials" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountIdentifier": "your-account-id",
"auth": {
"type": "jwt",
"privateKey": "",
"issuer": "your-issuer",
"subject": "your-subject",
"keyId": "your-key-id",
"scope": "adcp:read adcp:write",
"tokenEndpointUrl": "https://idp.example.com/oauth/token",
"audienceUrl": "https://agent.example.com"
}
}'
```
Body fields:
| Field | Type | Notes |
| - | - | - |
| `accountIdentifier` | string | Unique account ID at the source (1..255 chars) |
| `auth` | object | API key or JWT credentials. Required for non-OAuth sources. |
| `marketplaceAccount` | boolean | Admin-only. When `true`, creates a marketplace account instead of a client account. |
**Auth tokens grant AdCP access on your behalf.** Treat `auth.token` (and any
private keys) as a production secret:
* Store it in a managed secret vault. Apostra itself stores it in Google Secret
Manager and references it via `auth_secret_ref`.
* Never log the raw token, never echo it to stdout, and never commit it to
source control.
* Rotate on any suspected exposure and on a documented schedule.
* Use placeholders like `` in documentation and sample
requests — never a real key.
### Registering an account for a source
When you want a source to bill or report against a specific advertiser
in your account, register an account with `advertiserId`:
`POST /storefronts/:storefrontId/sources/:sourceId/accounts`
```bash theme={null}
curl -X POST \
"https://api.apostra.com/api/v2/buyer/storefronts/42/sources/src_main/accounts" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"advertiserId": "12345",
"accountIdentifier": "acme-brand-account",
"auth": {
"type": "api_key",
"token": ""
}
}'
```
Body fields:
| Field | Type | Notes |
| - | - | - |
| `advertiserId` | string | Numeric advertiser ID to associate with this account |
| `accountIdentifier` | string | Unique account ID at the source (1..255 chars) |
| `auth` | object | Credentials. Required for API\_KEY/JWT sources, omitted for OAuth |
### OAuth flow for OAuth-secured sources
Some inventory sources use OAuth. In that case, the credentials endpoint
isn't enough — the buyer must complete an interactive consent flow, and the
source agent exchanges an authorization code for tokens via the storefront
OAuth-authorize endpoint.
The OAuth endpoints sit on the `/api/v2/storefront/...` mount because they
power the storefront's auth integration on behalf of buyers and sellers
alike. Buyers usually start with the buyer source-credentials route above;
when a source requires OAuth, follow the returned authorization flow instead
of collecting secrets in your app.
**Step 1 — Request an authorization URL.**
```bash theme={null}
curl -X POST \
"https://api.apostra.com/api/v2/storefront/agents/agt_premium_ctv/accounts/oauth/authorize" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountIdentifier": "acme-brand-account"
}'
```
`accountIdentifier` is optional — when omitted, the platform uses
`oauth_` as the placeholder identifier and you can rename the
account later.
```json Response theme={null}
{
"authorizeUrl": "https://idp.example-publisher.com/oauth/authorize?client_id=...&redirect_uri=https%3A%2F%2Fapi.apostra.com%2Fapi%2Fv2%2Fstorefront%2Foauth%2Fcallback&state=pending_abc123&scope=adcp%3Aread+adcp%3Awrite",
"state": "pending_abc123"
}
```
**Step 2 — Send the operator to `authorizeUrl`.** Open it in a popup or new
tab. The publisher's IdP authenticates the user, asks them to consent, and
redirects back to the platform-hosted callback
(`/api/v2/storefront/oauth/callback`).
**Step 3 — The platform finishes the exchange.** The callback page exchanges
the authorization code for tokens, stores them under your account + the
source's underlying agent, and notifies the opener window via
`postMessage({ type: 'oauth-complete' })` before auto-closing. Refresh
`GET /api/v2/buyer/storefronts/:storefrontId` after that. The storefront's
rolled-up `connected` state should update once the required credential setup is
complete.
If you need to drive the callback yourself (for example, in a non-browser
agent), `POST /api/v2/storefront/agents/:agentId/oauth/callback` accepts the
`code` and `state` directly.
The seller side of OAuth (callback URLs, `state` handling, secret rotation)
is documented in [Storefront onboarding](/v2/setup/storefront-onboarding).
### Adapter-routed storefront providers
Some storefronts route directly to a wired provider adapter instead of an
inventory-source sales agent. These adapter-routed storefronts still appear to
buyers through the normal storefront discovery and media-buy surfaces. Most
connect through delegated OAuth; bearer-only providers such as OpenAI Ads use
the adapter credential flow instead.
Supported wired adapter providers are:
| Provider | `provider` value | Notes |
| - | - | - |
| Amazon | `amazon` | Sales adapter |
| AudioStack | `audiostack` | Creative adapter |
| Google Ads | `google` | Sales adapter |
| LinkedIn Ads | `linkedin` | Sales adapter |
| Meta | `meta` | Sales adapter |
| OpenAI Ads | `openai_ads` | Sales adapter; account-scoped bearer key, both creative formats, CPM/CPC/oCPC, and a dated nine-market country/region/DMA capability snapshot with live geo validation |
| Pinterest | `pinterest` | Sales adapter |
| Reddit | `reddit` | Sales adapter |
| Snap | `snap` | Sales adapter |
| Spotify | `spotify` | Sales adapter |
| TikTok | `tiktok` | Sales adapter |
Provider OAuth apps should allow the adapter callback path:
```text theme={null}
https://api.apostra.com/oauth/adapters/{provider}/callback
```
Use the same path on staging, for example
`https://api.staging.apostra.com/oauth/adapters/snap/callback`.
## Endpoint reference
| Method | Path | Purpose |
| - | - | - |
| `GET` | `/storefronts` | List storefronts (paginated) |
| `GET` | `/storefronts/:storefrontId` | Get a single storefront |
| `GET` | `/storefronts/:storefrontId/capabilities` | Inspect source-level capability diagnostics |
| `GET` | `/storefronts/discover` | Lightweight list of all storefronts (active + upcoming) |
| `GET` | `/storefronts/interest` | Check your interest submission status |
| `POST` | `/storefronts/interest` | Submit interest in one or more storefronts |
| `GET` | `/storefronts/credentials` | List your credentials across all storefronts |
| `POST` | `/storefronts/:storefrontId/sources/:sourceId/credentials` | Register credentials for a source |
| `POST` | `/storefronts/:storefrontId/sources/:sourceId/accounts` | Register an account (with advertiser linkage) |
When a storefront isn't returning products as expected, check `connected` via
`GET /storefronts/:id` first. If it is `false`, credential or account setup is
not complete. If it is `true`, use `GET /storefronts/:id/capabilities` for
source-level diagnostics, then fall through to the
[Discovery](/v2/guides/discovery) flow.
## Related
End-to-end setup for a new buyer account.
Once your sources are connected, run discovery to find products.
Advertisers are what you link to source accounts.
Operating a storefront? Start here for the seller-side setup.
# Organization settings in place
Source: https://docs.apostra.com/v2/organization/settings-in-place
Manage organization accounts, members, and billing without switching accounts.
# Organization settings in place
Organization settings live in **Account settings**, in the same rail as the rest of your settings, in every account. There is no separate organization page and no organization button in the account switcher.
Organization administrators can open **Account settings → Organization** while working in the organization or one of its child accounts. **Accounts** shows active and archived accounts and lets administrators add accounts and manage each account's lifecycle. **Members** manages organization access. **Plan & billing** is the organization's one billing home, so it no longer also appears under the account's own settings.
* In the organization itself, **Plan & billing** opens the full Plan & billing page, and **Members** is the only member list; the account section does not repeat either one.
* In a child account, **Plan & billing** opens the organization's full Plan & billing page right there, without switching accounts. Payment methods, plan selection, contracts and media billing changes all apply to the organization. Payouts stay per storefront: the **Payouts** tab shows the child account's own payouts, read-only, notes that the organization manages them, and offers an **Open** action for that organization's billing, which switches into the organization to change them. The child account's own **Members** stays under the account.
* Older links to the account's Plan & billing or, in the organization itself, its Members open the matching **Organization** pane.
* Older billing links (`/billing`, `/admin/billing`, `/storefront-billing` and the Billing view) open Plan & billing in one step, on the tab the link asked for. The setup checklist's **Payment & billing** row opens the same pane.
* Opening the organization itself takes an organization administrator straight to **Account settings → Organization → Members**. Older Organization settings links (`/org-settings`) open the matching **Organization** pane in one step. Organization members who are not administrators see the organization's accounts that they belong to, read-only, and open one from the account switcher.
### API keys and Advertisers
In the organization itself, an organization administrator finds **API keys** and **Advertisers** under **Organization**, and the account section no longer repeats **API keys**. **API keys** holds the organization's API credentials. **Advertisers** is the organization's advertiser roster, which includes the advertisers of its accounts. The **API keys** and **Advertisers** pages, and older links to the account's API keys, open these panes in one step.
In a child account, **API keys** stays under the account and shows that account's own credentials, and the **Advertisers** page stays where it is. Open the organization itself to manage the organization's keys and roster.
Members who are not administrators keep the pages they use today.
## Standalone accounts
A standalone account has one settings context, so its administrator sees the same functions in the account section, without an **Organization** label: **Accounts**, **Team**, **Plan & billing**, **API keys** and **Advertisers**. **API keys** and **Advertisers** pages open those panes in one step, and administrators also see any advertiser-scoped keys the **API keys** page listed. Older standalone Organization settings links open the matching Team or account-level Plan & billing section.
**Accounts** is where a standalone administrator adds another account. Adding one also creates an organization that manages both accounts; the dialog explains what moves to the organization and asks you to confirm before anything changes. Creation follows the [account-capacity rules](/v2/account-capacity).
## The account switcher
The account switcher in the top bar only switches between accounts. It lists the accounts you can open and any pending invitations, and it appears only when there is somewhere else to go: an account other than the one you are in, or an invitation. An organization is never an entry of its own; working in the organization itself, you open one of its accounts from the switcher. If you work in one account and have no invitations, it does not appear.
Adding an account, organization members and organization billing are not in the switcher; they are in **Account settings**. Switching to another organization opens one of its accounts you chose from the list, never an account picked for you.
## Who can use it
| You are | What you see under **Organization** |
| - | - |
| A direct administrator of the organization | **Accounts**, **Members**, **Plan & billing**, and, in the organization itself, **API keys** and **Advertisers** |
| A member of the organization who is not an administrator | No Organization section. Opening the organization lists the accounts you belong to, read-only. |
| A member or administrator of a child account only | One read-only row, **Managed by** followed by the organization name. It opens a limited notice that identifies the organization managing billing; it does not expose organization members, account lists, contracts, rates, payment information, or organization standing. |
| An administrator of a standalone account | No Organization label; the same functions appear in the account section (see [Standalone accounts](#standalone-accounts)). |
Advertiser-scoped access, API keys and service credentials never show the Organization section. Every organization pane and action also checks your access on the server, so a link or bookmark cannot open organization data you could not otherwise see.
Apostra platform administrators who are not members of an organization do not get the Organization section in it. Opening such an organization shows them its administration page instead.
# Ask Murph
Source: https://docs.apostra.com/v2/setup/ask-murph
Murph is the in-product assistant that helps storefront operators set up, merchandise, and run their storefront
Murph is the assistant built into Apostra platform for storefront operators.
It works alongside you in chat to set up your storefront, connect inventory
sources, compose products, and surface the analytics you need to negotiate and
sell. Murph is available to every storefront account — open the **Ask Murph**
panel from your storefront workspace to start a conversation.
The chat shows **You are interacting with an AI agent.** above the message box
before you send your first message. The line remains visible throughout the
conversation.
This page is written for storefront operators. Buyers have Murph too — with
buyer-side powers, a buyer Dashboard, and the same **Your requests** list —
and can also work directly through the
[Buyer API](/v2/buyer-introduction) and the Merchandising Agent.
## What Murph helps with
| Area | What Murph does |
| - | - |
| **Storefront setup** | Walks you through onboarding, captures your [business profile](/v2/object-guides/storefront), and gets your storefront ready to sell. |
| **Connecting inventory** | Sends you to the secure forms for ad servers like Google Ad Manager, FreeWheel, SpringServe, and AdsWizz, then verifies the connection. |
| **Setup documents** | Reads uploaded brand books, media kits, rate cards, and operating instructions so you don't have to paste them into chat. |
| **Merchandising** | Helps compose products and tune negotiation defaults using your recent storefront outcomes. |
| **Seller analytics** | Surfaces win rate, buyer asks, top products, and recommended negotiation posture so you can see what's working. |
| **Sandbox testing** | Runs and reports on [sandbox test plans](/v2/features/sandbox) so you can validate behavior before going live. |
| **Shared rooms** | Turns a conversation into a [shared room](/v2/features/murph-shared-rooms) teammates in your organization can join. |
| **Diagnostics** | Opens source health, ADCP Calls, test runs, and change history so product questions can be answered from live evidence. |
## Connecting your ad server
Murph guides you through connecting an upstream inventory source rather than
asking you to hand over secrets in chat. For password- or token-based ad
servers, Murph links you straight to the secure credential form, waits for you
to submit, then runs a connection test to confirm the source can authenticate.
For Google Ad Manager, no password is needed at all: Apostra creates a
service account dedicated to your account, and you grant that service-account
email access inside your GAM network. Before walking you through the grant, Murph explains
what access Apostra needs, what it will not do, and what the granted role
allows — and asks for your explicit consent first.
Apostra stores only non-secret display fields for a connection. The upstream ad
server holds the encrypted secret and mints short-lived tokens as needed. See
[Storefront onboarding](/v2/setup/storefront-onboarding) for the full
connection flow.
## Uploading setup documents
You can upload PDFs, decks, spreadsheets, images, and text documents during a
conversation. Murph summarizes them into structured facts instead of copying
the raw contents back, so it can reference your brand book, rate card, or
do-not-air list in later turns without re-reading the whole file.
For brand books, Murph can identify brand-manifest candidates — name, website,
colors, fonts, tone, tagline, and disclaimers — and help you draft or compare
the fields that map into a `brand.json`. When you upload or link to canonical
brand artifacts, Murph compares what it learns against the current AAO
brand.json state and calls out new information, changed values, conflicts, and
assets that still need public URLs. For logo images, Murph can upload the asset
to AAO for review; pending uploads are tracked but are not used in `brand.json`
until AAO approves and lists the public `/assets/brands/...` URL. Murph can
preview the exact `brand.json` update and, after you confirm it, publish it to
AAO for your verified storefront brand domain.
For media kits, advertiser policies, and insertion orders, the offline scan also
produces separate setup candidates when the document supports them: a business
profile patch, complete acceptance-policy markdown (including every category
the document says needs review), and URL-free AdCP 3.1 creative-format
declarations. Murph presents each candidate for confirmation. Nothing is
silently applied, and confirming one candidate does not approve the others. If
a policy is incomplete or too long for complete inline review, Murph marks it
as not writable and sends you to the AI Business Rules page and original
document instead of applying partial text.
### File attachments
Murph accepts file attachments on a per-turn basis, with these limits:
* Creative media files (MP4, QuickTime, MP3, WAV, and M4A) and ZIP creative
bundles are accepted up to **50 MB** decoded size. Every other attachment
type is capped at **5 MB** decoded size.
* The total decoded payload across all files on a single turn is capped at
**50 MB**, even when you attach more than one file.
Media uploads are treated as transient creative assets for the current turn.
Murph hands them to creative tools through `murph-attachment://` placeholders.
For MP3, WAV, and M4A audio up to **7 MB**, Murph also transcribes the upload so
you can talk through a request or ask about spoken content; the transcript is
treated as untrusted user content. Transcription uses Gemini 3.5 Flash through
Apostra's governed Vertex AI connection; it does not use an uploader-supplied API
key. Larger audio files remain available as creative assets but are not
transcribed inline. The raw media bytes are never decoded as text.
Attachments are not persisted to conversation history, so if a follow-up turn
needs the same file, attach it again or first write it into durable Apostra
state through a creative tool.
### How much of a document Murph reads
The size limits above are about what you can upload. Separately, there is a
limit on how much of a document's extracted text Murph carries into a single
turn, along with the other context it assembles for that turn — the page you are
on, your open requests, and the state of the conversation.
Each of those is bounded at 24,000 characters, which is above the largest
context Apostra produces in practice, so a normal document, page, or
conversation fits whole. Murph prioritizes what it extracts, so if a very long
document does exceed the limit, the structured setup facts it found — brand
fields, acceptance-policy categories, creative formats — are kept ahead of
lower-priority detail.
If Murph does hit that limit, it tells you its view of the content is
incomplete rather than answering as though it had read everything. If you see
that, ask about a specific section, or split the document and upload the part
you want reviewed.
## When Murph says it did not find something
A "no" from Murph is scoped to what it actually searched, and it says which
that was — "nothing in the documentation I searched", or "no request recorded
on this account". It is not a statement that the thing does not exist. Murph's
tools reach the product documentation, your account, and your requests; plenty
of the answer space sits outside them.
So when Murph reports not finding something, read it as *"not here"*, not
*"nowhere"*. If the answer might sit somewhere Murph cannot reach, it says so
and points you at who could confirm it — another part of the product, or Apostra team. It points you at someone who can *check*, which is not the same as
promising they already have the answer. Asking Murph to file a request is
always available if you would rather have a person look.
## Usage availability
Murph does not stop normal chat at a 24-hour token allowance. An unusually high
amount of automated or runaway usage can still trigger a temporary safety
pause. If that happens, Murph shows when you can try again. Gemini-backed chats
also keep tool instructions compact instead of repeating expanded instructions
during multi-step work.
## Slack command usage data
When someone in your workspace uses `/apostra` or the compatible `/murph`
command, Apostra records the workspace and channel IDs, command name, Slack
user ID, timestamp, retry count, and whether the command completed or failed.
We use this information to investigate delivery problems and to count unique
command users for the 30- and 90-day adoption reports that govern `/murph`'s
retirement. We do not store the command text, files, profile details, response
content, or error text in this usage record.
These records are available only to authorized operators. Apostra keeps them
while the workspace's Slack installation is active. Removing the Slack app
retires the installation and deletes its command-usage records, including Slack
user IDs.
## Stopping a response
While Murph is working on a turn, a **Stop** control appears in the composer.
Pressing it ends the turn right away instead of making you wait for it to
finish — useful when a request is taking longer than you want, or when you'd
rather rephrase and ask again.
Stopping halts Murph at the next step in its work. A step already underway — a
model response or a tool call that is mid-flight — runs to completion; Murph
stops before starting the next one.
Stopping does **not** undo work Murph already completed on that turn. If Murph
had already made a change before you pressed Stop — for example, applying a
setting or saving a value — that change stands. Stopping only prevents the
remaining steps. The reply on a stopped turn says so. To control a durable
write *before* it happens, use [Murph
confirmations](/v2/setup/murph-confirmations), where protected writes wait for
your explicit approval.
A chat response for a halted turn includes `stopped: true`. It is omitted on
turns that run to completion.
## Seller analytics and merchandising
Ask Murph for seller analytics and it can show your recent performance — win
rate, buyer asks, top surfaced products, and the commercial outcomes attributed
to your discovery runs. Murph turns that history into directional negotiation
guidance and a recommended posture, and the same recent outcomes can tune the
Merchandising Agent's negotiation defaults when composing products.
Your human operating instructions always remain authoritative over these
historical defaults — Murph's analytics inform the suggestion, they don't
override your rules.
## Your requests and support
An **ask** is anything you are waiting on Apostra for. Ask Murph to show your
current requests to see them in one view, grouped by kind:
* **Support** — problems, confusion, and blockers. Active support asks can show
when the next update is due and whether you have confirmed that you are still
blocked.
* **Product** — ideas and feature asks tracked for product review. Tracking an
ask is not a commitment to build it. When you state a clear product idea,
Murph adds it to this list automatically; you do not need to file a separate
support ask or repeat the request. If the same ask is already open,
Murph keeps the existing item instead of creating a duplicate. If product
tracking is not available for your account or Murph cannot verify the write,
it says that the ask was not confirmed instead of claiming it was recorded.
* **Supply (buyer accounts only)** — seller, property, and channel inventory a
buyer wants Apostra to carry.
* **Integration** — a counterparty we do not connect to yet, named by vendor.
* **Commercial** — pricing, terms, billing, and rate-card exceptions. Kept
apart from support so a commercial question is not queued as an outage.
### What each status means
Every ask uses the same short status list. Open statuses describe Apostra’s progress. **Done** and **Closed** describe Apostra’s outcome only; your confirmation remains separate.
| Status | What it means |
| - | - |
| `received` | Apostra recorded the ask. |
| `accepted` | Apostra acknowledged or planned the ask, without promising a delivery date. |
| `in_progress` | Apostra is actively working on the ask. |
| `done` | Apostra verified that the requested change or customer-visible result is live and is waiting for your confirmation. |
| `closed` | Apostra recorded an explicit no-change or cancellation outcome; you can acknowledge or dispute it. |
Each ask also includes a customer-facing label and explanation. Internal workflow states, ticket names, and engineering identifiers are not shown.
For support, product, and commercial asks linked to delivery work, closing an issue or merging a pull request is not enough. The ask reaches **Done** or **Closed** only after the explicitly linked work appears in a verified production release. It then remains in your recent history for 90 days with the release version and date, what changed or why no change was made, what you should do next, and delivery status. Supply and integration asks can also finish from their own customer-visible live-state evidence.
### Reading your asks from the API
Your own agent or integration can read the same list directly:
```http theme={null}
GET /api/v2/asks?limit=10
```
The response is read-only and scoped to your account from the API key you call
with — there is no customer parameter, and you can only ever read your own
asks. Each item carries `statusKey` (the values above) plus `statusLabel` and
`statusDescription` for display, and a call returns at most 25 asks alongside
`hasMore`. When `hasMore` is true, pass `nextCursor` back as `cursor` to read
the next page. See the
[buyer](/v2/buyer-api-reference) and [storefront](/v2/storefront-api-reference)
API references for the full shape.
On `/mcp/v3`, use `search` with `kind: "ask"`, then pass the returned opaque id
to `get` with the same kind. Record your own answer with `save_ask`, the opaque
id, and `requester.state`; ask content and Apostra's status remain read-only.
When you explicitly ask for a person, Murph prepares the support ask first — it
does not make you repeat the request or wait through diagnostics. For a blocker
without a direct human request, Murph uses available diagnostics or safe
recovery steps before preparing the ask. Either way, customer-authored filings
wait for your approval before they create team-visible work.
The ask content and Apostra status are read-only and scoped to your account.
Tell Murph whether the requested outcome happened, you acknowledge a no-change
answer, you are still blocked, or you want to withdraw the ask. Internal
routing identifiers are not shown. If one source cannot be read, Murph labels the view
as partial — the API reports that source in `unavailableKinds` — instead of
saying that you have no requests. A bounded result may also say that it is
showing the first set of requests rather than implying the list is complete.
Every kind shows open asks plus explicitly fulfilled asks from the 90-day recent
history described above. During the transition from the older support ledger,
some existing support asks may not yet include the next-update or still-blocked
details, but every returned ask has an opaque id.
### Compatibility payload for older clients
Older chat clients receive support-ask filing results in fields named
`escalated` and `escalation`. These are deprecated wire-format keys, not a
second customer object or lifecycle. Render the object as an ask, and check
`linearVisibility` before rendering any legacy Linear reference.
```json theme={null}
{
"escalated": true,
"escalation": {
"linearIdentifier": "MURPH-123",
"linearUrl": "https://linear.app/scope3/issue/MURPH-123/example",
"linearVisibility": "visible",
"slackPosted": true,
"severity": "medium"
}
}
```
`linearVisibility` can be:
| Value | Meaning |
| - | - |
| `visible` | `linearIdentifier` and `linearUrl` may be shown to the caller. |
| `hidden_for_role` | Linear exists, but the current caller is not allowed to see the Linear reference. Treat `linearIdentifier` and `linearUrl` as hidden, not missing. |
| `unavailable` | Linear was not configured, failed, or did not return a reference. |
After Murph files it, ask Murph to show your current requests. In Apostra,
the same tracker is available from the **?** menu and opens directly without
filing another chat turn. In Slack, ask Murph for the status in the thread where
you filed the request.
## Working in the Dashboard
Ask Murph includes dashboard surfaces so you can review your state without
leaving Murph.
Sellers can open **Dashboard** from the storefront rail, or ask Murph for seller
analytics. It opens the current seller analytics widget in chat, showing recent
performance, buyer brief outcomes, delivery, and merchandising guidance from
the same storefront analytics surface.
Buyers see a separate **Dashboard** view with Overview, Reporting, Activity,
and Creatives, plus an advertiser selector to
scope each tab to a single advertiser. Buyers also get a **Browse storefronts**
item in the sidebar that opens the full marketplace browse page inside the
Murph shell.
## Working in Diagnostics
Ask Murph also includes a **Diagnostics** view under the **Help** menu. Use it
for the storefront-wide change history and protocol-call evidence behind a
product or setup question.
The **Calls** tab is an account-wide feed of recent ADCP protocol activity for
connected sales agents. Do not treat an unfiltered account feed as evidence for
one Source. For a specific Source, follow **Agent → Inventory sources → \[your
Source] → Open full diagnostics → Calls**. If you need a direct link to the
account Diagnostics Calls view for that exact Source, include its source ID:
```text theme={null}
https://app.apostra.com//murph?view=diagnostics&diagnosticsTab=debug-calls&debugSourceId=
```
For an actionable health verdict and a no-spend `get_products` test against a
third-party sales agent, open the Sales Agent Diagnostics app instead:
```text theme={null}
https://app.apostra.com//murph?view=chat&askMurph=open_feature&feature=source-health
```
When you ask about a sales-agent source in Slack, Murph provides the same link
as an **Open sales-agent diagnostics** button because Slack cannot render the MCP
app inline.
For the full workflow, see [Diagnose third-party sales agents](/v2/storefront/inventory-sources/diagnostics).
## Apostra in Slack
Apostra can join a shared Slack channel with your team. A channel
is connected to exactly one account; once connected, Apostra answers
account questions there with that account's data.
Your Scope3 contact connects the channel by asking Apostra in the channel
itself, and confirms the account before anything changes. This works in a
Slack Connect channel whether your organization or Scope3 created it. Until
the channel is connected, Apostra answers only from public documentation and
uses no account data.
In a direct message, Apostra selects an account only when the domain in your
Slack email exactly matches one active account's approved domain and you
currently have an active seat on that account. Personal-email domains,
unapproved or inexact domain matches, missing or revoked seats, and multiple
eligible accounts do not select an account. If Apostra cannot verify those
conditions, including when an access check is unavailable, it fails closed and
does not use account-specific data until your access can be verified.
In Slack, the app and bot appear as **Apostra**. Tag `@Apostra`, use
`/apostra`, or send Apostra a direct message when you want a response. The
existing `/murph` command still works, so saved instructions and old channel
habits do not need to change.
In a channel message, the leading mention is the addressee: a message that
starts by tagging another person stays ambient even if it mentions Apostra
later. You can include a supported file with an `@Apostra` request or in a DM
for Apostra to review. A top-level file uploaded to the shared channel without a
caption is treated as ambient channel content, so Apostra stays silent. Ordinary
replies between people stay ambient too, even when they include account-specific
troubleshooting details.
Apostra uses the Slack thread as the durable conversation. When a follow-up is
posted as a new top-level message instead of inside the original thread, Apostra
also reads a small window of nearby channel messages so it can understand what
the follow-up refers to. Those nearby messages are context only; they never
change which account or tools the speaker is allowed to use.
That thread and channel window is bounded, the same way document and page
context is (see [document context limits](#how-much-of-a-document-murph-reads)).
A long thread fits whole. Two things can make the view partial: the thread's
text exceeding the window, or the thread having more replies than Slack returns
in one read (50). In either case Apostra says its view of the thread is incomplete
instead of inferring what it cannot see — so if an early message matters and
Apostra has flagged the thread as partial, paste or link it rather than assuming
it was read.
Apostra also uses the channel name, topic, thread, and connected-account context
to resolve informal company or product names before declaring them unknown. If
one candidate is clear, Apostra names that interpretation and answers from the
available account or documentation evidence. If several candidates remain, it
asks which one you meant. Context can help find the right record, but it never
grants access or supplies identity for an account change.
Not every participant in a shared channel has to be an Apostra user. If a
participant's Slack email is not linked to an Apostra account, Apostra can
still answer general product and documentation questions using the nearby
channel context. That includes public storefront availability: a participant
can ask for a public brand, storefront, or domain by name, and Apostra
can report the matching marketplace-listed storefront's public status,
channels, and regions. Apostra does not need an account connection or an exact
domain for that lookup. It cannot read account data, file reports, save
preferences, or make changes for that participant. Account-specific work
requires a linked Apostra user. Apostra links unlinked participants to Apostra signup and access-request flow. In a privately connected account
channel, the participant receives a private **Request access from admin**
button without re-entering their email; Apostra uses the email supplied by Slack
and asks that account's admins to approve the request. Shared multi-company
channels do not infer an account from the channel, so they continue to use the
email-verified signup flow.
Because a Slack channel can include people from outside your organization,
Apostra regularly checks who can read each connected channel. If the members of
a channel span **more than one account** — for example, a channel
shared between a buyer and a publisher they work with — Apostra pauses
account-specific answers in that channel and says so, rather than showing one
account's data to another company. General product questions and DMs are
unaffected. An account admin reviews the participants in **Communications**:
they can send an account invitation with an explicit role, keep someone as a
channel guest, identify an external company, or disconnect the channel. Until
that review resolves the unexpected participant, Apostra keeps account data and
changes unavailable in the channel.
### Deliberately shared channels
Some channels are shared with another company on purpose — for example, a
channel between a buyer and a publisher they work with. An Apostra admin can
tell Apostra in that channel to mark it as **shared** and name each participating
company. Apostra resolves the companies, shows their buyer or seller roles, and
records one acting account plus the expected counterparties. For a
buyer–seller channel, the buyer is the acting account and the seller is the
counterparty. If the roles are ambiguous, Apostra asks the admin to choose
instead of guessing.
Declaring a buyer–seller channel is also demand evidence: the buyer has a
working relationship in which it wants that seller's supply. Apostra
automatically tracks the seller's marketplace-visible storefront relationship in the buyer's
**Supply I would buy** view. Those relationship-tracked rows stay attached to
the shared-channel declaration and cannot be removed as if they were a manual
request. In a shared channel, Apostra:
* **Discusses the relationship freely** — supply you've asked to track for
that counterparty, whether it's ready, and general product questions.
* **Keeps everything else out of the channel.** Your other sellers,
campaigns, advertisers, spend, and account settings are never discussed
there — Apostra redirects those questions to a DM or your private channel.
Supply tracking in the channel works only for the counterparty's own
domains.
* **Lets you approve specific exceptions.** If a campaign of yours is
relevant to the shared work, you can tell Apostra to share it in the
channel. Apostra asks you to confirm explicitly — "Is it okay to share
this campaign's details here?" — and only someone from the owning
company can approve. Once approved, its settings-level details (name,
status, flight dates, budget) stay discussable in that channel until you
say "stop sharing it here." Every approval and revocation is recorded.
* **Keeps watching the room.** If someone from a third company appears in
the channel, Apostra pauses account answers again until an admin reviews.
The end-to-end flow for standing up a storefront and connecting inventory.
The storefront resource, business profile, and seller analytics fields.
Test plans and diagnostics for validating your storefront before launch.
Bring teammates into a Murph conversation with a shareable room link.
Find source health, recent ADCP Calls, and missing diagnostic gaps.
Set Murph's default language and read display preferences for the current
user.
Set a Murph conversation's account or advertiser scope.
Navigate the platform dashboard for members, keys, and accounts.
# Buyer Onboarding
Source: https://docs.apostra.com/v2/setup/buyer-onboarding
End-to-end setup guide for agencies and brands buying media through Apostra API
This guide walks you end-to-end from "I have an API key" to "my first campaign is live." It is intended for agency teams, in-house brand marketers, and engineers building automated media-buying workflows on top of Apostra.
## Overview
By the end of this guide you will have:
* Confirmed your buyer identity, chosen a plan, and accepted the platform Terms of Service.
* Created an advertiser, linked to a brand identity.
* Configured measurement and (optionally) registered event sources for performance optimization.
* Run a discovery to find products that match your brief.
* Created, executed, and monitored your first campaign.
Prefer to drive everything through an AI agent? You can do every step below by talking to a connected MCP agent (Claude, ChatGPT, Cursor) — the agent will call these endpoints for you. See the [Quick Start](/v2/quickstart) for connector setup.
***
## Request access before you have an account
Every buyer door — bringing your own agent or code over MCP and the API, and
working through Claude, ChatGPT or our own chat and UI — is in closed beta. You
ask to join the waitlist, we review the request, and you complete terms and
payment setup once you are let in. The self-serve journey described below is
how getting in works once the waitlist comes off, not what you meet today.
Before account setup, Apostra may ask which Buyer or Seller product,
country, channel, and supply path you need. Product readiness, market
availability, and current supply are shown separately. If a product or market
cell is not ready, you can record an email notification for that exact cell.
If only Marketplace supply is missing, you can still continue, bring or connect
supply, browse another cell, or record the notification.
A waitlist submission records demand interest only. It does **not** create an
account, confirm ownership of the email domain, or grant product access. It also
does not put a future Account into a sandbox or pending lifecycle. When you
continue signup, email verification and Organization proof resolve or create
the Organization and Account; only ambiguous, conflicting, disputed, or risky
claims require manual review.
Choosing email notification records the exact disclosure text, locale, and
version you accepted. It remains pending and cannot send email until your
principal is verified. Before the intake is claimed, resubmitting the same
email updates the exact scoped interest. After claim, sign in to update
Organization interests; the public form will not modify reviewed evidence.
If your organization already has an Apostra account, an invitation or an
approved organization-access request remains the way you receive access. See
[Account Access & Signup](/v2/setup/account-access) for those paths.
***
## Prerequisites
### Pick how you'll authenticate
Pick **one** — you don't need both.
Connecting through Claude (Team/Enterprise/Desktop), ChatGPT MCP Connectors, or any OAuth-aware AI tool. Auth is handled automatically via the connector — no API key to manage. See [Built for Agents](/v2/setup/built-for-agents) for connector setup.
Required only for CLI tools, Cursor, custom MCP/HTTP clients, or programmatic backend integrations. Generate at [app.apostra.com/user-api-keys](https://app.apostra.com/user-api-keys); keys start with `scope3_`. See [Authentication](/v2/authentication) for full details.
The rest of this guide shows REST `curl` examples with `Authorization: Bearer $SCOPE3_API_KEY`. If you're using OAuth via Claude/ChatGPT, replace each step with the equivalent natural-language prompt — the connected agent will call the same endpoints for you.
### Optional
If your organization uses SAML SSO, ask your admin to configure it via the [SSO Setup guide](/v2/setup/sso-setup) before inviting team members.
Some storefronts gate inventory behind per-source credentials (API key, OAuth, JWT). Register those at the storefront-source level — see the [Storefront object guide](/v2/object-guides/storefront#registering-credentials-per-source) for the walkthrough.
All REST examples in this guide use the buyer base URL:
```
https://api.apostra.com/api/v2/buyer
```
***
## End-to-End Flow
Verify the API key resolves to the account you expect. The current account is the one that will own the advertisers and campaigns you create.
```bash theme={null}
curl -X GET "https://api.apostra.com/api/v2/accounts/current" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
Response:
```json theme={null}
{
"data": {
"id": 1234,
"company": "Acme Media",
"name": "Acme Media",
"role": "ADMIN"
}
}
```
If your team is structured as a parent organization with sub-brands or sub-agencies, list every account you can switch into with `GET /api/v2/accounts` and switch using `POST /api/v2/accounts/switch`. All subsequent API calls are scoped to the active account.
An account administrator must also confirm whether this buyer account represents
the whole buying operator or a specific operating unit. A whole-operator account
uses the operator domain alone. A specific unit adds a stable
`operator_unit.id`, such as an established buying-seat, office, region, or team
code. Review the other buyer accounts under the same operator before claiming
the whole domain, and never use `default`, a temporary account name, a seller's
account ID, or an Apostra database ID as the unit ID.
Open Buyer Setup through your connected agent, or use the buyer readiness API,
to review and confirm this identity. See [Buyer setup and go-live](/v2/buyer/account/setup)
for the identity rules, existing-account migration behavior, and the distinction
between account name, billing company, and stable unit ID.
Open Buyer Setup and continue to Plan & Billing to select the plan for this
Buyer Account. Plan is an Account × product fact: it does not prove your
Organization identity, accept Terms, provide payment authority, or guarantee
that a particular market or supply source is available.
**Signed in via the web app?** If you registered or signed in at [app.apostra.com](https://app.apostra.com), you accepted ToS during that flow — skip this step and go straight to Step 3.
If you signed up with an API key directly (not via the web app) and your billing
organization does not yet have an active contract, real operations remain blocked
until an organization administrator accepts the latest Platform Service Agreement:
```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/accept-tos" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"psaVersion": "v1.0"
}'
```
Until ToS is accepted, the Account is limited to the documented demo and onboarding
surfaces. Real operations return HTTP `403` with an actionable setup error such as
`TOS_ACCEPTANCE_REQUIRED`.
If you enter through a bring-your-own buyer agent registration, every allowed
V3 MCP tool result appends this warning while the registration is still in its
signup sandbox or its customer-bound Organization has not accepted current
Terms: "Demo/onboarding environment: demo data is synthetic and has no
commercial effect. Do not rely on demo results for commercial decisions or
present them as market data. Do not enter confidential or personal
information. Full notice:
[https://apostra.com/agreements/demo-environment-notice](https://apostra.com/agreements/demo-environment-notice)". MCP hosts also
receive the same notice in
`_meta["scope3/demo-environment-warning"]`, with
`environment: "demo_onboarding"`, `demoData: "synthetic"`, and
`commercialEffect: false`, plus a machine-readable `noticeUrl`. Once current
Terms are accepted, the warning stops and the Account's normal capability
checks govern each operation.
**Discovering the current `psaVersion`**: use the `psaVersion` returned in the `TOS_ACCEPTANCE_REQUIRED` error response from any blocked write, or check the most recent agreement posted at [apostra.com](https://apostra.com/agreements/terms-of-service).
An **advertiser** is the top-level container for campaigns. It carries brand identity, default optimization mode, and (optionally) UTM parameters and frequency caps that flow down to its campaigns.
```bash curl theme={null}
curl -X POST "https://api.apostra.com/api/v2/buyer/advertisers" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp",
"description": "Global advertising account for Acme Corporation",
"brand": "acme.com",
"optimizationApplyMode": "MANUAL",
"sandbox": false
}'
```
```typescript TypeScript theme={null}
const res = await fetch(
'https://api.apostra.com/api/v2/buyer/advertisers',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SCOPE3_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Acme Corp',
brand: 'acme.com',
optimizationApplyMode: 'MANUAL',
sandbox: false,
}),
},
)
const { data: advertiser } = await res.json()
```
Key fields:
| Field | Description |
| - | - |
| `name` | Display name for the advertiser (1–255 chars). |
| `brand` | Domain (e.g. `acme.com`) or full manifest URL. Identity is resolved from `/.well-known/brand.json` or the AdCP brand registry. |
| `optimizationApplyMode` | `AUTO` applies Apostra AI optimizations automatically; `MANUAL` requires approval. Defaults to `MANUAL`. |
| `sandbox` | When `true`, all downstream ADCP calls run against sandbox-flagged accounts — no real spend. **Immutable** after creation. |
| `linkedAccounts` | Optional partner-platform accounts to link at creation (e.g. Snap, Google). |
| `utmConfig` | Click-URL parameters appended to landing-page URLs at delivery. None unless you set them. |
| `frequencyCaps` | Buyer-side frequency caps Apostra enforces across publishers. |
Building automation? Set `sandbox: true` for your first end-to-end run so nothing hits real exchanges or wallets.
Measurement configuration lives on the advertiser and is shared across all of its campaigns. You can opt into MMM, incrementality testing, brand lift, and other measurement programs.
```bash theme={null}
curl -X PUT "https://api.apostra.com/api/v2/buyer/advertisers/12345/measurement-config" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mmmEnabled": true,
"mmmConfig": {
"provider": "measured",
"reportingFrequency": "weekly"
},
"brandLiftEnabled": false
}'
```
To optimize campaigns against your own conversion events, register one or more **event sources** for the advertiser. This pushes ADCP-spec event source definitions into Apostra:
```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/buyer/advertisers/12345/event-sources/sync" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account": { "account_id": "12345" },
"event_sources": [
{
"event_source_id": "website_pixel",
"name": "Acme Web Pixel",
"event_types": ["page_view", "add_to_cart", "purchase"],
"allowed_domains": ["acme.com", "shop.acme.com"]
},
{
"event_source_id": "crm_uploads",
"name": "Acme CRM Uploads",
"event_types": ["purchase", "lead"]
}
]
}'
```
Each event source must include a buyer-assigned `event_source_id` (1–255 chars). You'll reference it later when sending conversion events via the [Conversion API](/v2/integrations/conversion-api).
Once registered, an event source can be referenced from a campaign's `performanceConfig.optimizationGoals` to drive bid optimization.
Full conversion-tagging and measurement setup is covered in the [Measurement guide](/v2/guides/measurement-incrementality).
Discovery turns a natural-language brief and budget into a session of candidate products from every reachable sales agent.
```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/buyer/discovery/discover-products" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"advertiserId": "12345",
"brief": "Premium CTV inventory targeting tech enthusiasts in the US",
"budget": 50000,
"flightDates": {
"startDate": "2026-05-01T00:00:00Z",
"endDate": "2026-05-31T23:59:59Z"
},
"channels": ["ctv", "video"],
"countries": ["US"]
}'
```
The response contains a `discoveryId` (e.g. `disc_abc123`), grouped products (e.g. `prod_xyz789`), agent results, and (optionally) curated `proposals` you can apply directly to a campaign.
Discovery is iterative — call `POST /api/v2/buyer/discovery/:id/discover-products` again with `refine` to narrow results, or use `POST /api/v2/buyer/discovery/:id/apply-proposal` to load a curated set.
The full discovery workflow — refinement, proposals, manual product selection — is covered in the [Discovery guide](/v2/guides/discovery).
A campaign references an advertiser and a (preselected) discovery session. The
minimum required fields are `advertiserId`, `name`, `flightDates`, and `budget`.
The legacy `routingType` field is not a campaign input; Apostra derives it
per media buy as temporary billing compatibility metadata from `BillingParty`.
It is never a storefront or execution type and does not determine campaign
mode, BYOA, or protocol connectivity. See
the [Campaign guide](/v2/object-guides/campaign) for details.
```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/buyer/campaigns" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"advertiserId": "12345",
"name": "Acme Q2 CTV Launch",
"flightDates": {
"startDate": "2026-05-01T00:00:00Z",
"endDate": "2026-05-31T23:59:59Z"
},
"budget": { "total": 50000, "currency": "USD" },
"constraints": {
"channels": ["ctv", "video"],
"geo_countries": ["US"]
},
"discoveryId": "disc_abc123",
"productIds": ["prod_xyz789"]
}'
```
Response:
```json theme={null}
{
"data": {
"campaign": {
"campaignId": "cmp_67890",
"advertiserId": "12345",
"name": "Acme Q2 CTV Launch",
"status": "DRAFT"
}
}
}
```
Capture the `campaignId` from the response — we'll use this `cmp_*` id in Steps 7–8.
Key choices:
| Field | Notes |
| - | - |
| `discoveryId` + `productIds` | Attach a discovery session and pre-select products. Optional but typical. |
| `performanceConfig` | Add optimization goals tied to event sources or pacing metrics. Goals are hints — they don't lock composition. Set `autonomy.inventorySelection: "automatic"` separately to delegate composition to the platform optimizer. |
| `pacingPeriods` | Time-based pacing schedule with weight or fixed budget per period. |
| `audienceConfig` | Target / suppress audience IDs from the signals catalog. |
The campaign is created in `DRAFT` status — nothing has been sent to a sales agent yet.
Before a campaign can launch, every product format selected on the campaign needs at least one creative that satisfies it. The creative endpoint is a single multipart request — a JSON `metadata` field plus one or more `files` parts:
```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/buyer/campaigns/cmp_67890/creatives/create" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: multipart/form-data" \
-F 'metadata={"name":"Hero 30s CTV Spot","template_id":"video_standard","url_asset":{"url":"https://acme.com/promo","url_type":"CLICKTHROUGH"},"assets":[{"filename":"hero-30s.mp4","asset_type":"VIDEO","label":"main_video"}]};type=application/json' \
-F "files=@hero-30s.mp4"
```
Inspecting `GET /api/v2/buyer/campaigns/:id` returns a `creativeFormats` summary so you can see which formats are still missing creatives before launch. See the [Creatives guide](/v2/object-guides/creative) for the full lifecycle.
Executing a campaign sends media-buy requests to every selected sales agent and transitions the campaign from `DRAFT` to `ACTIVE`.
```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/buyer/campaigns/cmp_67890/execute" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
Response:
```json theme={null}
{
"data": {
"campaignId": "cmp_67890",
"previousStatus": "DRAFT",
"newStatus": "ACTIVE",
"success": true
}
}
```
If any media buys fail at the sales agent, `success` is `false` and `errors[]` lists the per-buy failures. After fixing the issue, you can re-run `POST /api/v2/buyer/campaigns/:id/execute` while the campaign is still `DRAFT`. The retry submits the same surviving DRAFT media buy; it does not rebuild the buy from discovery or discard per-buy creatives, flight dates, pacing, or optimization goals you changed after the failure.
`execute` and `reactivate` are different endpoints. Use `POST /api/v2/buyer/campaigns/:id/execute` to launch a `DRAFT` (or `COMPLETED`) campaign. To resume a `PAUSED` campaign, use `POST /api/v2/buyer/campaigns/:id/reactivate` instead.
Need to pass `debug: true`? Add `{ "debug": true }` to the body — Apostra returns extra trace data from each upstream agent.
***
## Monitoring
Once a campaign is `ACTIVE`, poll the live status of its media buys:
```bash theme={null}
curl -X GET "https://api.apostra.com/api/v2/buyer/campaigns/cmp_67890/media-buy-status" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
The response includes per-media-buy status pulled directly from each ADCP sales agent, plus a list of agents queried and any status changes detected on this poll. Pair this with the [Reporting overview](/v2/guides/reporting-overview) for delivery, spend, and performance metrics.
To pause a running campaign:
```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/buyer/campaigns/cmp_67890/pause" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
***
## Sandbox Testing
Set `sandbox: true` when creating an advertiser to run the entire flow above with no real spend. All downstream ADCP calls are tagged sandbox and routed to test accounts on the storefront's inventory sources. Sandbox mode is immutable after creation — you cannot promote a sandbox advertiser to production.
See [Sandbox mode](/v2/features/sandbox) for the full sandbox capability matrix.
***
## Next Steps
Linked accounts, brand resolution, UTM templates, and frequency caps.
Performance config, pacing, audience targeting, and execution lifecycle.
Briefs, refinement, proposals, and product selection.
Conversion events, MMM, incrementality, and brand lift.
Delivery, pacing, spend, and performance reporting endpoints.
Browse storefronts and connect credentials per inventory source.
# Murph confirmations
Source: https://docs.apostra.com/v2/setup/murph-confirmations
Handle explicit approval for Murph durable-write actions
Murph can propose durable writes while helping operate a storefront, such as
filing a report, updating operating instructions, publishing storefront setup
state, or changing other persistent configuration. For protected writes, Murph
returns a structured confirmation request instead of executing the write
immediately.
Clients should render the confirmation as an explicit approve/cancel control.
The control exists only when the response contains a structured pending
confirmation; assistant prose cannot create one. For reports, integration
requests, and asks, Murph uses this staged control as the filing offer, and no
team-visible record exists until the approved replay succeeds.
On Murph UI-control surfaces, an ordinary-language reply that clearly approves
the action—such as "yes", "yep", "sounds good", or an equivalent in the user's
language—is also accepted for the exact pending action. Murph interprets the
reply semantically; ambiguous, conditional, or conflicting replies do not
execute. Phrase-token API/MCP flows still require the exact confirmation phrase.
Filing an ask offers the control before anything reaches Apostra team, and it
always does so when the same conversation has already read content Murph did not
author — an uploaded document, a search result, or a response from another agent.
Text from those sources can contain an instruction, and your approval is what
stops such an instruction from putting a request in Apostra queue that looks
like you sent it. Murph also looks into a problem before offering to file it, so
on a broken-or-blocked ask the approval offer arrives after that check rather
than instead of it.
## Confirmation lifecycle
1. Send a normal Murph chat request.
2. If the response includes `pendingConfirmations` (or `pendingConfirmation`),
show the proposed action and an approve/cancel control for each entry.
3. POST each user decision to the confirmation decision endpoint.
4. If the user confirms, the response is a normal Murph chat response for the
turn that executed the approved write.
5. If the user cancels, the response only records the cancelled confirmation.
**Prefer `pendingConfirmations` (plural).** Murph can propose multiple gated
writes in a single turn (e.g. queue N `delete_campaign` calls). Older clients
that only read `pendingConfirmation` (singular) render just the first widget
and silently drop the rest. New clients should iterate `pendingConfirmations`
and render one approve/cancel control per entry; each entry has its own
`confirmationUid` and is decided independently.
`confirmationUid` is short-lived, single-use, and tied to the authenticated
actor, conversation, tool, and resolved parameters. Do not persist it as a
long-term approval record.
## Chat response fields
`POST /api/v2/murph/chat` and `POST /api/v2/murph/chat/stream` can return
two related fields on a turn where one or more protected writes were
proposed but did not run:
* **`pendingConfirmations`** *(array, new)* — every pending confirmation
issued this turn, in tool-call order. Render one approve/cancel control
per entry. Empty (or omitted) when nothing gated this turn. **New
callers should prefer this field.**
* **`pendingConfirmation`** *(object, retained)* — the first entry from
`pendingConfirmations`, or `null`. Kept for back-compat with clients
that render at most one confirmation per turn (Slack, older SDKs). A
client that reads only this field will silently drop the remaining
N-1 controls when Murph gates multiple writes in one turn.
```json theme={null}
{
"data": {
"conversationUid": "6e60d909-721e-41ca-85b8-4e5a72bbf792",
"answer": "This action needs approval before it runs.",
"toolsUsed": [],
"escalated": false,
"escalation": null,
"customerSwitch": null,
"pendingConfirmation": {
"confirmationUid": "50669fad-52e1-43c8-9334-77bf14ba72eb",
"toolName": "save_ask",
"toolKey": "save_ask",
"summary": "Run `save_ask`.",
"policy": "always",
"resolvedParams": {
"title": "Checkout page error",
"severity": "low"
},
"writeExecuted": false,
"plainAffirmativeAccepted": true,
"expiresAt": "2026-06-13T19:45:00.000Z"
},
"pendingConfirmations": [
{
"confirmationUid": "50669fad-52e1-43c8-9334-77bf14ba72eb",
"toolName": "save_ask",
"toolKey": "save_ask",
"summary": "Run `save_ask`.",
"policy": "always",
"resolvedParams": {
"title": "Checkout page error",
"severity": "low"
},
"writeExecuted": false,
"plainAffirmativeAccepted": true,
"expiresAt": "2026-06-13T19:45:00.000Z"
}
]
}
}
```
Each entry (in either field) uses this shape:
| Field | Type | Notes |
| - | - | - |
| `confirmationUid` | UUID | Pass this to the decision endpoint. Each pending confirmation gets its own UID; a multi-write turn yields N distinct UIDs. |
| `toolName` | string | Internal Murph tool that requested the write. |
| `toolKey` | string | Stable key for the gated action. |
| `summary` | string | Human-readable action summary for the confirmation UI. |
| `policy` | `"always"` or `"tainted"` | Why the confirmation gate applied. |
| `resolvedParams` | object | Parameters the user is approving. |
| `writeExecuted` | `false` | The write did not run on this turn. |
| `plainAffirmativeAccepted` | boolean | When `true`, a semantically clear ordinary-language approval can redeem this exact pending UI action; ambiguous or conflicting replies do not execute. Phrase-token flows remain `false`. |
| `expiresAt` | datetime | Confirmation expiry time. |
## Confirm or cancel
`POST /api/v2/murph/confirmations/{confirmationUid}/decision`
```bash theme={null}
curl -X POST "https://api.apostra.com/api/v2/murph/confirmations/50669fad-52e1-43c8-9334-77bf14ba72eb/decision" \
-H "Authorization: Bearer $SCOPE3_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "decision": "confirm" }'
```
## Request body
| Field | Type | Required | Notes |
| - | - | - | - |
| `decision` | `"confirm"` or `"cancel"` | Yes | Confirms or cancels the pending write. |
## Confirm response
When `decision` is `confirm`, the endpoint returns the same shape as a Murph
chat response. The approved write may appear in `toolsUsed`, and fields such as
`escalated` or `customerSwitch` reflect the write that actually ran.
If the approved tool returns an error, Murph distinguishes outcomes only when
the tool can prove the failure happened before any durable write could take
effect. In that case Murph says that nothing was written and can offer a fresh
approval after the cause is corrected. For timeouts, transport failures, server
errors, and tools that provide no proof, Murph reports the outcome as unknown
and asks you to verify the current state before retrying. This conservative
default prevents an already-applied write from being repeated after its response
was lost.
## TARS PM investment decisions
TARS PM-seat conversations in Admin and ordinary chat use the same confirmation
lifecycle when an accountable human adopts, prioritizes, defers, reorders,
resizes, or declines a displayed investment recommendation. TARS first reads
the canonical recommendation for the selected project or initiative, then
stages `tars_confirm_pm_investment_decision` with the exact proposal,
disposition, target order, and any owned readiness action.
The pending confirmation is bound to the authenticated actor, conversation,
selected project or initiative, and canonical proposal targets. A confirmed
replay uses that stored scope; clients cannot replace it by sending a different
scope or target. The resulting decision records whether it came from Admin or
ordinary chat, but that provenance is resolved by the server rather than
accepted from client input.
A registered Slack investment-recommendation thread records the same canonical
decision semantics through its own server-validated thread and
accountable-human authority. It does not issue or redeem the pending
confirmation described on this page. Slack provenance is likewise resolved by
the server, never supplied as decision input.
Confirming an investment decision does **not** file an issue, add a
`build-room-ready` label, nominate work, request admission, launch a workspace,
or allocate compute. A request to cross one of those boundaries is a distinct
execution transition with its own authority and confirmation requirements.
These PM-seat tools are available only on authenticated internal TARS surfaces.
They are not general Murph campaign-management tools and do not change the
account or advertiser scope of a Murph conversation.
```json theme={null}
{
"data": {
"conversationUid": "6e60d909-721e-41ca-85b8-4e5a72bbf792",
"answer": "Filed the report.",
"toolsUsed": [
{
"name": "save_ask",
"input": {
"title": "Checkout page error",
"severity": "low"
},
"output": "{ \"status\": \"filed\" }",
"isError": false
}
],
"escalated": true,
"escalation": null,
"customerSwitch": null,
"pendingConfirmation": null
}
}
```
## Cancel response
When `decision` is `cancel`, no write runs and the response only records the
cancelled confirmation.
```json theme={null}
{
"data": {
"confirmationUid": "50669fad-52e1-43c8-9334-77bf14ba72eb",
"status": "cancelled"
}
}
```
## Errors
* `400 VALIDATION_ERROR` - the path or request body is malformed.
* `404 NOT_FOUND` - the confirmation is unknown, expired, cancelled, or already
used.
* `403 FORBIDDEN` - the confirmation belongs to another actor or account.
* `503 SERVICE_UNAVAILABLE` - Murph is not available to process a confirmed
write.
See [Errors](/v2/reference/errors) for the full error contract.
## Related
Use Murph to operate and troubleshoot your storefront.
Authenticate Murph API requests.
# Storefront Onboarding
Source: https://docs.apostra.com/v2/setup/storefront-onboarding
End-to-end guide for sellers/publishers to onboard their storefront, register inventory, and start accepting agent traffic
The v2 API is in active development. The onboarding flow described here may evolve before general availability.
## Explore a sample Storefront before signup
You can open [Explore a sample Storefront](https://app.apostra.com/explore/sample-storefront) before
creating an account. It is a public, stateless walkthrough with versioned,
preloaded fictional data. Its Catalog, fictional buyer brief, and approval path
are display-only: there are no text fields, uploads, saved changes, buyer
messages, pricing actions, or production-system connections.
The sample is not a workspace or a Demo Storefront. It does not create an
account, tenant, source, or demo lease, and it cannot publish inventory, buy,
sell, bill, or contact a buyer. Create a Seller Account and accept the Terms
before adding your organization's material or connecting a production system.
## Confirming your company (Media Company signup)
This pre-application step is enabled for a controlled cohort while Apostra
validates the underlying research and admission policy. Signups outside the
enabled cohort continue directly into the flow described in the rest of this
guide, with no gap in access.
Before you reach account setup, Apostra identifies your organization from the
work email you signed up with and asks you to confirm it. This is a separate,
one-time admission step — distinct from **Step 1 "Verify your company"**
below, which resolves your public AAO brand profile after your account
already exists.
1. **Identity proposal.** From your email's domain, Apostra proposes a company
name and website: *"From `acme.com`, we think you're Acme Media. Is that
accurate?"* Confirming reuses your verified sign-up email domain as
evidence of control. Correcting the domain to something else is recorded as
an unverified claim — a claimed domain is never treated as proof of
ownership, so an unrelated domain does not by itself grant your account any
authority over it.
2. **Seller product.** Choose **Just list** or **Create an AMC account**. Just
list uses an agent operated by you, a partner, or another provider. An AMC
account includes listing and adds a hosted Merchandising Agent that you
train for your business. The choice remains editable after onboarding.
3. **Billing country.** You confirm the country of the company that will
contract with Apostra — not your personal location. This country is the
sole input to the plan currency Apostra derives and displays back to you;
there is no currency picker, and an unsupported country never silently
falls back to USD. If your country isn't supported yet, you're shown an
assisted path to contact Apostra directly instead of a dead end.
4. **Terms of Service and Privacy Policy.** Baseline platform Terms
acceptance and Privacy Policy disclosure happen here, before any setup
content. A paid rate card is a separate, later acceptance once you choose a
plan.
5. **Research and admission.** Confirming starts an automatic background
check — normally a few seconds. You'll see a brief "Checking your
details" state; if it runs long, the page tells you it's still working, and
you can safely close it — refreshing the page or logging back in always
resumes the same status rather than restarting the question.
Once admitted, your applicable setup workspace already treats company identity
as complete. Just-list operators continue to the Agents workspace to connect
the agent they or their provider operates; an Agentic Media Company continues
to the inventory-source and merchandising steps below.
Admission at this stage
means your organization may operate a Media Company workspace at all; it is
never a marketplace listing, Critical Supply, or Market Maker decision — those
remain separate reviews later in the storefront lifecycle.
See [Confirm Media Company identity](/v2/storefront/tasks/confirm-media-company-identity)
for the request/response schema, status values, and error codes behind this
step.
## Overview
A **Storefront** is your buyer-facing home on Apostra: the business presence, name, description, and discovery surface buyers use to understand who they are buying from. Your **Merchandising Agent** runs that Storefront, implements the AdCP media-buy workflow, and draws from the inventory sources you connect.
### Choose a commercial package
Signup asks what you are buying from Apostra, and you can change the plan
later:
* **Listing** — get listed on Apostra, connect an AdCP-compliant
sales agent that you or a provider operates, and keep the operational record
for campaigns, media buys, creatives, approvals, delivery, and activity. AI
Business Rules are available. Saving, enabling, and evaluating AI Business
Rules is not an IU-rated activity today; other qualifying activity remains
governed by your organization's accepted IU Rate Card.
* **Listing + Distribution** — keep that connected agent while adding self-serve
advertisers, public listing distribution and an optional customer CNAME, and
customer-branded AdCP and ChatGPT app channels.
Listing + Distribution is offered only when the active Rate Card publishes it.
Review and accept the published package price in Plan & billing before it
takes effect.
Signup also shows two private contact paths: **Enterprise — Listing** and
**Enterprise — Merchandising**. Neither is a public or selectable Rate Card
plan. Choose **Get in touch** to discuss either path; your plan, price, and
access do not change unless your organization later accepts a separate private
offer.
This is commercial packaging, not a behavioral mode. The `setupIntent` field
remains a reversible record of the selected signup package for compatibility,
but it never suppresses setup steps, navigation, tools, analytics, or unrelated
capabilities. Listing keeps operational surfaces and AI Business Rules available
and presents **Add Listing + Distribution** for paid distribution capabilities.
Merchandising remains separate and requires a connected source that is ready to
supply products your storefront can sell, such as an ad server — a
wholesale-capable source that has not finished its own setup does not satisfy
this yet. Source treatment remains per source (`WHOLESALE`,
`COMPOSING`, or `BOTH`). Who operates a connected sales agent is Source
configuration, not a different Listing plan or account profile; a third-party
sales-agent source with Listing is simply `COMPOSING`.
### Choose your seller product
Supply-side signup asks which product you want to start with:
* **Just list** — list inventory with an agent operated by you, a partner, or
another provider. You can connect the agent during setup.
* **AMC account** — listing plus Apostra's hosted Merchandising Agent, which you
train for your business. The Merchandising Agent can draw from an ad server,
custom modular source, external Agent, or linked Storefront and uses your
merchandising guidance to package, price, and sell inventory.
You can change this selection later. It controls which workspaces and setup
requirements apply; it does not classify your company permanently, decide how
an individual Source is treated, or grant a paid merchandising entitlement.
Just list starts by connecting the agent operated by you or your provider;
there is no inventory-source prerequisite for that connection. A Just list
account has the **Inventory**, **Buyers**, and **Agents** workspaces. Inventory
is where you follow your sources, media buys, approvals, and activity.
Connecting an ad server through an Apostra-managed sales agent is part of an
AMC account, not Just list; to use one, switch to Agentic Media Company.
Agentic Media Company setup starts by connecting inventory sources, adds the
**Advertisers** workspace, and gathers the advertiser context that guides the
hosted Merchandising Agent.
To switch products after onboarding, open **Settings → How would you like to
sell?** and choose Just list or Agentic Media Company. You can also ask Murph to
make the change. An integration can make the same audited change by sending
`operatingMode` to `PATCH /api/v2/storefront`. The update replaces the listing
and Apostra-merchandising capability pair together, and the applicable
navigation changes without deleting completed setup or changing your plan.
Switching to Just list is refused when the organization's accepted plan does
not cover a Listing-only storefront, for example on an Agentic Media Company
plan or while the organization holds Buyer Accounts. The refusal names the plan
it needs; see
[Which plan holds which accounts](/v2/setup/account-access#which-plan-holds-which-accounts).
Switching to Agentic Media Company is the first step of an upgrade and is
allowed on a Listing plan.
A Seller Account created outside supply-side signup, for example with **Add
account** or `POST /api/v2/accounts/create-child`, starts on the seller product
the request sends in `operatingMode`. Without one, it starts on the seller
product the organization's accepted plan covers: Agentic Media Company on an
Agentic Media Company plan, otherwise **Just list**. Storefronts Apostra
provisions for an Apostra-managed ad server, and Scope3-operated adapter
storefronts, start as Agentic Media Company.
`PATCH /api/v2/storefront` is a **true partial patch**: a capability flag you
leave out keeps its stored value. `PUT /api/v2/storefront` is the aggregate
update and applies the whole capability object, so a flag you leave out of a
`PUT` is stored as `false`. Use the `PATCH` when you mean to change some flags
and leave the rest alone.
### What you declare vs. what buyers get
Capability is two values, and they can legitimately disagree:
* **`configuredCapabilities`** — the flags you declared. This is what you wrote,
and it is the field to compare against when you want to know whether a save
would change anything.
* **`capabilities`** — the **effective** projection buyers see. Derived from your
declaration plus your source topology and approval settings.
The derivation rules, all of them observable on the storefront read. Declaring
a flag `false` is never rejected — what differs is whether the *effective*
response reflects what you sent. Declaring a flag `true` can be rejected: see
*Approval-routing prerequisite* below for the exact rule before assuming every
save succeeds.
* **`capabilitiesLocked: true`** — you have ad-server-backed inventory (an
embedded sales agent). `offersCreativeReview` and `offersCampaignApproval`
are effectively on regardless of what you declared — the storefront *is* the
agent buyers address, so those workflows are always live. The lock does
**not** extend to `offersProductComposition`: it is derived the same way as
on an unlocked storefront (merchandising entitlement plus a ready wholesale
source or the ambient wholesale pool), so it can still be effectively off on
a locked storefront.
* **Product composition off** (no ad-server-backed inventory, no adapter) —
the storefront routes selling to your external sources on the Agent-supplied
path, so no Storefront-owned workflow is advertised: creative review,
campaign approval, and product composition are all effectively off even if
you declared them on.
* **Product composition on (no ad-server-backed inventory, no adapter)** —
`offersProductComposition` is on. `offersCampaignApproval` is derived from
`mediaBuyApproval` (on only when it's `manual`) — your approval setting is
the single source of truth for whether composed buys queue for review, so
declaring the flag does not override it. `offersCreativeReview` is exactly
what you declared.
* **Adapter-routed storefronts** — your declared flags are returned verbatim,
*except* for the social-platform adapters (LinkedIn, Meta, Pinterest, Reddit,
Snap, TikTok), where the platform owns creative and campaign acceptance
end-to-end: `offersCreativeReview` and `offersCampaignApproval` are always
off regardless of what you declared. `offersProductComposition` is still
returned exactly as declared on every adapter, social or not.
This is why a successful save is not a promise that buyer-facing capability
changed. Read `capabilities` back after a write, not just the flags you sent,
and treat `configuredCapabilities` as the record of your own declaration.
`setupIntent` is a compatibility record of the signup package, never a runtime
mode. It does not determine source treatment or hide product functionality.
### Approval-routing prerequisite
A save is rejected with a `400` (field-scoped: `mediaBuyApproval` for
media-buy approval, `creativeApproval` for creative review) under one rule,
applied independently for each of the two approval kinds:
> On an existing, non-adapter-routed storefront, if this write results in the
> approval setting (`mediaBuyApproval` or `creativeApproval`) being `manual`
> **and** the matching capability (`offersCampaignApproval` or
> `offersCreativeReview`) being effectively `true`, the write is rejected —
> **unless** that exact pair (approval already `manual` **and** capability
> already effectively `true`) already held immediately before this write, or
> routing already resolves for that kind: either you have an active primary
> approval-routing policy saved for it, or — when none is saved — the default
> routing to active organization admins resolves. Routing is opt-out, not
> opt-in: a storefront with no saved policy is still covered as long as an
> active organization admin exists to receive it.
That predicate is the whole rule — the examples below illustrate it, they do
not define when it can or cannot fire:
* **Plain storefront, turning a workflow on for the first time, with no
routing at all.** Product composition is effective, `creativeApproval` is
already `manual`, `offersCreativeReview` goes from `false` to `true` in this
write, there is no saved creative-review policy, and no active organization
admin exists to fall back to → rejected.
* **ESA-backed storefront, approval-setting transition.**
`offersCreativeReview` is always effectively `true` on an ad-server-backed
storefront, but if a separate write changes `creativeApproval` from `auto`
to `manual` and routing does not resolve for creative review (no saved
policy and no active organization admin), it is still rejected — the
capability's value never changed, but the (approval, capability) pair
became newly `(manual, true)` together.
* **Already active — resaving is a no-op.** If `mediaBuyApproval` is already
`manual` and `offersCampaignApproval` is already effectively `true`,
resaving the same values does not re-trigger the check, because that pair
already held before the write.
If you have an active organization admin, default routing already covers you;
otherwise, configure a primary approver before turning a workflow on for the
first time or before switching its approval setting to `manual`.
(Source: `apps/api/src/services/v2/storefront.service.ts:1904-1968`; routing
check: `apps/api/src/services/storefront-sources/approval-routing.ts:917-968`.)
For organizations using **Just list** or **Agentic Media Company**, the Storefront
journey has **four user-visible steps**, mirroring the in-app onboarding UI:
1. **Verify your company** — resolve your brand from the AAO registry, set your operator domain, and auto-verify (or fall back to manual KYC).
2. **Connect inventory sources** — register one or more inventory sources: an external sales agent, your own ad server with Apostra-managed sales-agent plumbing, or another Storefront.
3. **Set up settlement and payouts** — confirm the Seller Account currencies used
for settlement, then add payout bank details so Apostra can pay you by
bank transfer. Currency is part of go-live readiness. Payout details are
required to receive disbursements for normal Seller Accounts, but they never
block launch: funds accrue until the details are added. They are optional
for official Apostra sales-adapter Seller Accounts that already operate under a
downstream platform settlement agreement. Direct billing for
normal Seller Accounts is coming later and is not configurable today.
4. **Go live** — satisfy every current readiness requirement, including transaction proof for each active path in the compatibility-named `publish_validation` check. A third-party Sales Agent's current-revision proof is reusable across its Sources; an uncovered Source can run the public validation skill. The derived status becomes `live` automatically while the storefront is not paused; Apostra review remains a separate prerequisite for public buyer discovery.
A few helper endpoints support these steps but are not standalone "steps":
* `POST /resolve-brand` — looks up your brand in the AAO registry. Used inside Step 1 to pre-fill the form.
* `GET /discover-agents` — surfaces agents AAO knows about for your domain. Used inside Step 2.
* `GET /readiness` — the canonical status projection you can call any time to see what gates remain.
The AdCP specification and AAO registry define the protocol, registry records,
storyboards, and validation semantics. This guide explains how Apostra uses
those signals during setup: AAO **registration** blocks connecting an external
agent source; AAO **compliance** is surfaced as an advisory warning; publisher
`adagents.json` **authorization** is surfaced as an advisory setup/product signal
today; and marketplace listing is a separate Apostra review step after activation.
Each seller account receives **one storefront automatically** when the account
is provisioned, and each storefront can connect one or more inventory sources.
There is no `customerId` path parameter — the storefront is resolved from your
API key's account context.
### Using Apostra app
You do not need to create a storefront before starting setup in Apostra
app; it already exists when your seller account is ready. Open **Business
profile**, choose **Build my profile**, and tell Murph about your business,
inventory, channels, regions, and buyer-facing pitch. Murph will propose the
profile for your confirmation.
The remaining setup areas then guide you through connecting inventory, setting
your selling rules, testing the storefront, and resolving readiness blockers.
Buyer Setup and Seller Setup share the same status, progress, and operator
editing pattern. Seller Setup then adds storefront-only tracks for inventory,
publisher authorization, settlement, and **Get paid**; the last of those stays
visible without being counted as a launch blocker.
Setup time depends on the inventory sources you connect, their authorization
and compliance state, and whether your account is ready for billing and
activation; Apostra does not promise a fixed setup time.
Use the API flow below only when you are integrating programmatically.
### IU plan during the staging pilot
The Organization IU Rate Card is published in staging for a controlled pilot and
visible only to invited organizations. Public seller signup remains closed
until the same `iu-rate-card-pilot` flag is rolled out globally. Invited staging
organizations see the exact Rate Card revision on their next eligible login;
after global rollout, new organizations see it during signup as well. Signup
shows every public plan that applies to a Listing-only seller or to an Agentic
Media Company and sets your starting seller product from the plan you accept: a Listing
plan, or continuing without a paid plan, starts you as Listing only, and a
Storefront agent plan starts you as an Agentic Media Company. A paid plan chosen
at seller signup is accepted as part of signup, once your email address is
verified, before your Storefront is created a moment later in the same signup.
Only an accepted plan sets your starting product; a paid choice that is not
accepted leaves you on Listing only until you change it in Settings.
Accepting it creates an immutable record of the exact revision and plan accepted. An
admitted published activity may then draw from the accepted IU balance and
appear as usage during the pilot. Monetary charging, invoices, payment
collection, renewal charging, and separately controlled entitlement enforcement
remain off.
New Rate Cards show exactly three published activities: **Brief response** (1
IU per completed Apostra merchandising cycle), **Enhanced Reporting** (4
IUs per exact connected account per billing period after its control is enabled
and its existing reporting subscription completes a successful sync), and
**Apostra media buy** (1 IU per
qualifying non-social buy per billing period with positive impressions or
spend). The accepted version remains the authority; earlier accepted Rate Cards
keep their historical activities.
You can instead **Continue without a paid plan** or **Decide later**. Continuing
without a plan suppresses the automatic login prompt only for that exact Rate
Card revision; deciding later allows it to appear again on the next login. The
manual **Choose an IU plan** action remains available under **Settings → Plan &
Billing** while the offer is current. These plan choices are separate from the
payout details required for consolidated billing.
Plan & Billing shows only public plans that cover every active product in the
billing organization. For a Seller Account, the choices also follow the current
Storefront operating mode: Listing accounts receive listing plans, while Agentic
Media Companies can choose listing or merchandising plans. Buyer, Seller, and
Partner products in the same organization must all be covered by one plan; Apostra
will prepare a targeted offer when no public plan covers that combination. Existing
accepted plans remain in effect until the organization accepts different terms.
## Who this is for
* Publishers and sales houses connecting their inventory to agentic buyers
* Retail media networks exposing on-site or off-site inventory through AdCP-compatible agents
* Any seller who wants buyer agents (e.g. Apostra, Claude, custom buyers) to be able to discover and transact against their inventory
## Prerequisites
Generate a key at [app.apostra.com/user-api-keys](https://app.apostra.com/user-api-keys). Keys start with `scope3_` and authorize all storefront endpoints.
Your brand should have a `brand.json` published and resolvable through the AAO registry at `agenticadvertising.org`. If you don't have one yet, the resolve-brand call returns a `builderUrl` that points you to the registry's brand builder.
An external AdCP-compatible sales agent, an operator-owned ad server, or another Storefront. For external agents, you'll need the endpoint URL, protocol, and (for non-OAuth agents) auth credentials.
Required for consolidated billing. Have your bank details ready — beneficiary name and address, account number or IBAN, one bank identifier (Fedwire/ABA routing number, CHIPS ABA, SWIFT-BIC, or local bank code), and the currency your account accepts. Accounts under an organization inherit billing from the organization and do not set up their own. Direct billing for normal Seller Accounts is not configurable yet.
***
## Onboarding flow
The first thing a seller does is identify their company so Apostra can pull their brand profile from AAO and validate the operator domain. This step combines a brand lookup, a storefront update, and an automatic operator-domain verification check.
#### 1. Resolve your brand (helper)
Look up your brand in the AAO registry to grab the canonical brand name and logo URL. This call has no side effects — it's only used to populate the storefront update payload.
```bash curl theme={null}
curl -X POST https://api.apostra.com/api/v2/storefront/resolve-brand \
-H "Authorization: Bearer scope3_..." \
-H "Content-Type: application/json" \
-d '{ "domain": "acme.com" }'
```
```json Response (resolved) theme={null}
{
"resolved": true,
"domain": "acme.com",
"brandName": "Acme",
"logoUrl": "https://cdn.example.com/acme-logo.svg",
"manifestUrl": "https://acme.com/.well-known/brand.json",
"manifest": { "...": "full brand.json" },
"registryEntry": { "...": "AAO registry entry" },
"authorizedOperators": [
{ "domain": "acme-media.com", "scope": "primary" }
],
"houseBrand": false
}
```
If no manifest is found, the call returns `200` with `{ "resolved": false, "builderUrl": "https://agenticadvertising.org/brand" }`.
**What if my brand isn't found?** A `resolved: false` is not an error and does not block you — it just means you haven't published a `brand.json` yet. The storefront shows no resolved brand logo in that state; it never substitutes a logo inferred from your website or a third-party enrichment service. Publish one at the `builderUrl` (or host your own at `/.well-known/brand.json`) and re-run `resolve-brand`; we read it live. Your `brand.json` is your own identity document — we read it, we never own it. See [Identity documents](/v2/concepts/identity-documents) for what it declares and how it differs from publisher authorization (`adagents.json`).
The `domain` field is validated against a strict FQDN regex. IP addresses and internal hostnames are rejected to prevent SSRF.
#### 2. Write the brand fields onto your storefront
Seller account provisioning creates this storefront record automatically. You
can retrieve it with `GET /storefront`. `POST /storefront` remains idempotent for
programmatic recovery and returns the existing record rather than creating a
duplicate:
```bash curl theme={null}
curl -X POST https://api.apostra.com/api/v2/storefront \
-H "Authorization: Bearer scope3_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Media",
"publisherDomain": "acme.com",
"operatorDomain": "acme.com",
"plan": "basic"
}'
```
`POST /storefront` is idempotent — if a storefront already exists for your account, the existing record is returned instead of creating a duplicate.
`operatorDomain` is the canonical domain this storefront operates as and the identity buyers and AAO matching use for the storefront. It can differ from the account's registered `customerDomain`, and it can be left unset during setup if the operator is not known yet. A storefront cannot go live until an operator domain is set and verified. `publisherDomain` is optional storefront metadata and should not be used as the matching key for cross-publisher storefronts.
Then update it with the brand fields from `resolve-brand` plus your operator domain:
```bash curl theme={null}
curl -X PUT https://api.apostra.com/api/v2/storefront \
-H "Authorization: Bearer scope3_..." \
-H "Content-Type: application/json" \
-d '{
"operatorDomain": "acme.com",
"brandName": "Acme",
"logoUrl": "https://cdn.example.com/acme-logo.svg"
}'
```
```json Response theme={null}
{
"platformId": "acme-media",
"name": "Acme Media",
"publisherDomain": "acme.com",
"operatorDomain": "acme.com",
"brandName": "Acme",
"logoUrl": "https://cdn.example.com/acme-logo.svg",
"operatorDomainVerified": true,
"plan": "basic",
"status": "PENDING",
"createdAt": "2026-04-25T12:00:00.000Z",
"updatedAt": "2026-04-25T12:00:00.000Z"
}
```
When you set `operatorDomain`, the API first checks for an exact match against an approved `customerDomain`. If it does not exactly match, Apostra checks the alias/rebrand evidence path below. An Apostra verification request is needed only when neither path verifies the operator domain automatically.
If your storefront operates under an alias or rebrand domain, such as an `operatorDomain` that differs from your team's email domain, keep the canonical operator domain you want buyers to see. Apostra can auto-verify the alias when your registered account domain is already approved and trusted AAO or `brand.json` evidence connects the account domain and requested operator domain to the same organization or ownership chain. Useful evidence includes AAO registry org linkage, `brand.json` `authoritative_location`/house portfolio entries, and `authorized_operators`. AAO TXT records, `/.well-known/adagents.json`, AAO Partner membership, and website redirects help Apostra review the request, but a redirect is not ownership proof by itself.
**Your account domain is approved first, and its approval gates everything else.** Approve it by activating a member whose email matches that domain, or by requesting Apostra attestation for it. Until the account domain is approved, alias and rebrand evidence is not evaluated at all — so an unapproved account domain, rather than your published evidence, is a common reason an alias operator domain stays pending.
The `operator_domain` readiness check names which case applies and who acts next:
| Readiness reports | Who acts | What to do |
| - | - | - |
| The account has no registered domain | you | add the account domain, then get it approved |
| The account domain is not yet approved | you | activate a matching member email, or request Apostra attestation |
| No AAO or `brand.json` evidence links the domains | you | publish or correct that evidence |
| Apostra could not complete the evidence check | Apostra | nothing — the lookup failed or timed out, and it retries on the next storefront read |
The last row is not a finding about your evidence: your published evidence may already be correct, and there is nothing for you to fix.
The account `customerDomain` and storefront `operatorDomain` are intentionally separate fields. Updating `customerDomain` syncs the storefront operator domain only when the storefront has no operator domain yet or is still mirroring the previous account domain **and** has no populated operator-identity profile. If the storefront has an explicitly different operator domain, or changing a mirrored domain would clear its `description`, `channels`, `membershipStatus`, or `website`, the API preserves the storefront domain. Change it directly with `PUT /storefront`, where you can resupply the new operator's profile or explicitly confirm the reset.
`description`, `channels`, `membershipStatus`, and `website` describe the
current operator identity. When `operatorDomain` changes, the API refuses to
silently clear any populated values among those fields. The validation response
lists the affected fields.
Either resupply valid values for the new operator in the same `PUT`, or pass
`"confirmOperatorDomainProfileReset": true` to clear the unprovided values.
Explicitly resupplied fields are preserved or replaced; an unchanged
`operatorDomain` does not reset anything and needs no confirmation.
Common identity fields on `PUT /storefront` include `name`, `publisherDomain`,
`operatorDomain`, `brandName`, `logoUrl`, `logoBackground`, `description`,
`channels`, `membershipStatus`, and `website`. The endpoint also accepts the
storefront configuration fields in the API reference. Confirmation fields alone
do not count as an update; at least one mutation field must be provided.
`PUT /storefront` also accepts `subtitle`, `supportUrl`, `privacyUrl`, and
`termsUrl` — a short tagline and support/privacy/terms-of-service URLs for
this storefront. Unlike `website`, these are not tied to `operatorDomain`
identity and are never cleared by an operator-domain change. Every
marketplace channel listing (the ChatGPT app, a future Claude plugin)
projects these facts read-only instead of collecting a separate copy per
channel — see [Create a white-label ChatGPT app](/v2/setup/chatgpt-app-setup).
An **inventory source** connects a named slot inside your Storefront to something your Merchandising Agent can draw from: an external ADCP-compatible sales agent, an operator-owned ad server with Apostra-managed sales-agent plumbing behind it, or another Storefront. Buyer-side discovery surfaces your Merchandising Agent as the ADCP actor for the Storefront, and discovery or media-buy calls route through the active source behind it.
#### 1. Discover agents (helper)
Optional but recommended: see what AAO already knows about your domain. This proxies AAO's operator and publisher endpoints plus your `.well-known/adagents.json`.
```bash curl theme={null}
curl "https://api.apostra.com/api/v2/storefront/discover-agents?domain=acme.com" \
-H "Authorization: Bearer scope3_..." \
-H "x-aao-api-key: aao_..."
```
The `x-aao-api-key` header is optional. Without it you only get the public registry view. Pass it to also surface storyboard compliance status for agents you operate.
```json Response theme={null}
{
"domain": "acme.com",
"operator": {
"domain": "acme.com",
"member": { "slug": "acme-media", "display_name": "Acme Media" },
"agents": [
{
"url": "https://agent.acme-media.com/mcp",
"name": "Acme Media Sales",
"type": "SALES",
"compliance": {
"status": "passing",
"storyboards_passing": 12,
"storyboards_total": 12,
"headline": "All scenarios pass"
}
}
]
},
"publisher": {
"domain": "acme.com",
"adagents_valid": true,
"properties": [ { "id": "acme-app", "type": "mobile_app", "name": "Acme App" } ],
"authorized_agents": [
{ "url": "https://agent.acme-media.com/mcp", "authorized_for": ["display"] }
]
}
}
```
Responses are cached server-side for 2 minutes per `(domain, key fingerprint)`. Pass `&refresh=true` to force a re-fetch.
#### 2. Register an inventory source
```bash curl theme={null}
curl -X POST https://api.apostra.com/api/v2/storefront/inventory-sources \
-H "Authorization: Bearer scope3_..." \
-H "Content-Type: application/json" \
-d '{
"sourceId": "acme-sales",
"name": "Acme Media Sales",
"executionType": "AGENT",
"type": "SALES",
"endpointUrl": "https://agent.acme-media.com/mcp",
"protocol": "MCP",
"authenticationType": "API_KEY",
"auth": { "type": "bearer", "token": "agent_abc123..." },
"description": "Primary external sales agent for Acme on-site display + CTV"
}'
```
**Required when `executionType: "agent"`:** `type`, `endpointUrl`, `protocol`, `authenticationType`. **`auth` is required** for `API_KEY`, `JWT`, and `BASIC_AUTH` agents and **must be omitted** for `OAUTH` and `NO_AUTH`.
Inventory-source credentials (API keys, Basic usernames/passwords, and JWT private keys) are encrypted at rest and only referenced by an opaque `auth_secret_ref` in the database. They are **never echoed back** in API responses (the response surfaces `authConfigured: true` instead). Never log, screenshot, or commit raw credentials to source control. Rotate immediately if a credential is exposed.
```json theme={null}
{
"authenticationType": "API_KEY",
"auth": { "type": "bearer", "token": "agent_abc123..." }
}
```
Token formats `bearer`, `apikey`, and `api_key` are all accepted. The token is encrypted at rest and never echoed back. The source goes to `pending` and auto-activates once its credential is configured. Reachability is reported independently by source health and connectivity checks.
```json theme={null}
{ "authenticationType": "OAUTH" }
```
Omit the `auth` field entirely. The response includes an `oauth.authorizationUrl` you must redirect the operator to. Auth completes out-of-band; the source flips to `active` once the OAuth callback succeeds.
The OAuth `state` parameter Apostra attaches to the authorization URL is opaque, single-use, and carries CSRF entropy scoped to your account + source. Do **not** decode, modify, or replay it — the callback handler validates `state` server-side and rejects mismatches. If you proxy the redirect through your own server, forward `state` byte-for-byte.
```json theme={null}
{
"authenticationType": "BASIC_AUTH",
"auth": {
"type": "basic",
"username": "agent-user",
"password": "agent-password"
}
}
```
Username and password are encoded into the outbound HTTP `Authorization: Basic ...` header when Apostra calls the source. The raw credentials are encrypted at rest and never echoed back.
```json theme={null}
{
"authenticationType": "JWT",
"auth": {
"type": "jwt",
"privateKey": "-----BEGIN PRIVATE KEY-----\n...",
"issuer": "https://acme.com",
"subject": "acme-sales",
"keyId": "key-1",
"scope": "agent:invoke",
"tokenEndpointUrl": "https://auth.acme.com/oauth/token",
"audienceUrl": "https://agent.acme-media.com/mcp"
}
}
```
JWT private keys are long-lived signing credentials. Treat them like passwords — never log them, never commit them to source control, and rotate immediately if exposed. Submitted material is encrypted at rest and never echoed back.
```json theme={null}
{ "authenticationType": "NO_AUTH" }
```
Public agents only. The source is created with status `ACTIVE` immediately.
```json Response theme={null}
{
"sourceId": "acme-sales",
"name": "Acme Media Sales",
"executionType": "AGENT",
"status": "PENDING",
"agentId": "agent_01HX...",
"type": "SALES",
"endpointUrl": "https://agent.acme-media.com/mcp",
"protocol": "MCP",
"authenticationType": "API_KEY",
"authConfigured": true,
"createdAt": "2026-04-25T12:05:00.000Z",
"updatedAt": "2026-04-25T12:05:00.000Z"
}
```
AAO is checked at **connect time** and by the readiness projection, but the only hard AAO gate is connect-time **registration**. Connect-time requires that the agent is **registered** with AAO; failing or pending agents can still be connected. In readiness, AAO compliance is **informational** — a non-`passing` verdict is surfaced as a prominent warning but does not block transactions.
| AAO status | Connect a source | Transaction readiness |
| - | - | - |
| `passing` | OK | OK |
| `pending` | OK (logged) | OK — surfaced as an advisory warning |
| `not-passing` | OK (logged) | OK — surfaced as an advisory warning |
| `not-registered` | **Rejected** with `VALIDATION_ERROR` | n/a |
| AAO unreachable | **Rejected** with `SERVICE_UNAVAILABLE` — retry once it recovers | n/a |
The source still must be operational — the compatibility-named `agent_status` readiness check (see Step 4) reads the inventory source's canonical lifecycle and is a blocker for third-party sources. The connection sidecar and legacy `adcp_agent.status` value are not separate storefront lifecycle signals. AAO compliance (the `agent_connectivity` readiness check) is advisory: a non-`passing` verdict surfaces for review and does not leave the storefront `blocked`. Determining who may sell which publisher inventory stays a per-publisher authorization question against the publisher's own `adagents.json`, not this compliance score. The connect-time AAO registry gate runs uniformly for every caller — there is no SuperAdmin bypass.
Seller Accounts can connect as many external `AGENT` inventory sources as needed — no per-plan cap is enforced today. Ad-server-backed inventory sources (`MANAGED_SALES_AGENT`) are slot-exempt regardless of plan.
Updates are partial — `PUT /api/v2/storefront/inventory-sources/:sourceId` accepts any subset of `name`, `description`, `endpointUrl`, `protocol`, `authenticationType`, `auth`, `status`. Updating `auth` rotates the stored credential; omitting it preserves the existing one.
#### 3. How buyers discover this storefront
Once a Storefront is transacting, buyer discovery can surface its Merchandising Agent as a first-class ADCP sales agent with ID `storefront-{platform_id}`, where `platform_id` is the Storefront's public `platformId` slug. Buyers call the Storefront surface; they do not target inventory source IDs directly in buyer discovery.
`get_products` behavior depends on the storefront mode:
* **Composition** — the Merchandising Agent composes buyer-facing products from active ingredient sources plus the active operating instructions.
* **Passthrough** — the Merchandising Agent proxies `get_products` to an active source and returns the upstream products with Storefront identity overlaid.
Matching buyer instructions are resolved at `get_products` time using operator domain, brand domain, and optional country. Storefront-built storefronts apply them during composition; Agent-supplied storefronts apply them as response overlays, including discounts and notes. Without at least one active source, buyer `get_products` has nothing to compose from or proxy to.
Execution-specific configuration (per integration knobs — e.g. ad-server adapter settings for Apostra-managed ad-server sources) lives in typed fields per integration type, not in a generic config bag. New integration types add their own typed fields rather than overloading a polymorphic blob.
Ad-server-backed inventory sources (`executionType: "MANAGED_SALES_AGENT"`) wire your Storefront to one of four operator-owned ad-server adapters. Apostra manages the AdCP plumbing behind your ad server. Pick one when creating the connection via `POST /api/v2/storefront/esa`:
| Adapter | Credentials you supply | How Apostra handles them |
| - | - | - |
| **Google Ad Manager** (`google_ad_manager`) | Numeric network code. | Apostra provisions a service account dedicated to your account; you grant it access in your GAM admin console. |
| **SpringServe** (`springserve`) | Login email + password, or a pre-minted API token. New setup pins `springserve:v1` with the matching typed authentication method. | Forwarded to the managed ad-server source and encrypted there; only non-secret display configuration and the versioned contract selection stay on Apostra connection row. The source mints a fresh 2-hour token from email/password and auto-refreshes. |
| **FreeWheel** (`freewheel`) | Publisher API client ID + client secret. A 7-day temporary access key is also accepted for advanced testing. | Forwarded to the managed ad-server source and encrypted there; never written to Apostra connection row. The client ID/secret path mints and auto-refreshes short-lived tokens. |
| **AdsWizz** (`adswizz`) | Static API key, numeric agency id, and three-letter agency billing currency. | New setup pins `adswizz:v1` with fixed Domain/Forecasting endpoints and `x-api-key` authentication. The key is encrypted by the managed source and never written to Apostra connection row. |
Only non-secret display fields (login, environment, default advertiser/demand-partner id) are stored on the connection row so the UI can render "connected as ...". Rotating credentials on a live ad-server source preserves products, principals, and sync history via `PUT /api/v2/storefront/inventory-sources/{sourceId}/adapter-config`.
Some storefronts route directly to a wired provider adapter rather than an
inventory-source sales agent. Supported adapter provider values are `amazon`,
`audiostack`, `google`, `meta`, `pinterest`, `reddit`, `snap`, `spotify`, and
`tiktok`.
Delegated OAuth for these adapter storefronts uses the shared adapter callback
path:
```text theme={null}
https://api.apostra.com/oauth/adapters/{provider}/callback
```
Register the same path on staging when testing, for example
`https://api.staging.apostra.com/oauth/adapters/snap/callback`.
Reddit access tokens expire after one hour. Apostra requests permanent
authorization and uses Reddit's refresh token to renew access automatically. If
an existing Reddit connection reports expired credentials after one hour,
reconnect it once in **Settings → Connections**; new and reconnected grants
then refresh automatically.
For FreeWheel and SpringServe, prefer the storefront credential screen for
passwords and tokens rather than pasting secrets into chat. Murph can send the
operator directly to the secure form with
`/{accountId}/storefront?tab=sources&connectAdServer=freewheel` or
`/{accountId}/storefront?tab=sources&connectAdServer=springserve`. That link
opens Inventory sources, launches Connect ad server, and preselects the right
adapter. Murph can then wait for submission, list the ad-server sources to
find the new or updated connection, and run
`POST /api/v2/storefront/esa/{esaId}/test-connection` to verify that the
upstream source can authenticate.
For testing, use a temporary API token when the ad server supports one. For
production, the credential pair is usually better because the upstream source can
mint and refresh short-lived tokens automatically. In either case, Apostra stores
only non-secret display fields; the upstream source holds the encrypted
secret.
### Uploading setup documents to Murph
Murph can use uploaded PDFs, decks, spreadsheets, images, and text documents
during storefront setup. Use this for brand books, media kits, operating instructions,
rate cards, do-not-air lists, and other materials that would otherwise need to
be pasted into chat.
Uploaded documents are summarized instead of copied back verbatim. The
document-processing status includes:
| Field | Type | Description |
| - | - | - |
| `extractedFacts` | array | Structured facts extracted from the uploaded files. Each fact includes `type`, `content`, `confidence`, `entities`, and `tags` so Murph can reference the source material in later turns without re-reading the full file. |
| `brandManifestCandidate` | object \| null | Present when a brand book or visual identity document includes fields that may map into AAO `brand.json`. Includes `status`, `confidence`, `rationale`, `mappedFields`, `missingInputs`, and `recommendedActions`. |
For brand books, Murph can identify brand.json candidates such as name,
website URL, colors, fonts, tone, tagline, contact details, and disclaimers.
Logo images can be uploaded to AAO for review; pending uploads are not public
and are not written into `brand.json` until AAO approves and lists the
`/assets/brands/...` URL. Other assets still need public hosted URLs before
they can be used in `brand.json`.
Murph can draft and preview `brand.json` fields from an uploaded brand book,
then compare those fields against the current AAO brand.json state and publish
the confirmed manifest to AAO for your verified storefront operator domain.
Uploaded logo images can be sent to AAO review from Murph; only approved AAO
asset URLs or other public HTTPS URLs are written as logo or asset entries.
#### Google Ad Manager service-account grant
Google Ad Manager does not require the publisher to paste a password or API
token into Apostra. Apostra creates a service account dedicated to your account
and returns its email address from `POST /api/v2/storefront/esa/service-account`. The
publisher grants that service-account email access inside their GAM network,
then Apostra provisions the ad-server-backed source with the publisher's numeric
network code.
The operator-owned part of the flow is:
1. Call `POST /api/v2/storefront/esa/service-account` and copy the returned
`serviceAccountEmail`.
2. In Google Ad Manager, go to **Admin → Global settings → Network settings →
Add a service account user**.
3. Enter the service-account email returned by Apostra.
4. Grant a role that can read inventory and traffic campaigns, such as
`Trafficker` or a least-privilege custom role with equivalent API
permissions.
5. Wait a few minutes for the grant to propagate.
6. Create the ad-server source with `POST /api/v2/storefront/esa` and body
`{ "type": "google_ad_manager", "networkCode": "12345678" }`.
If the probe returns `ADAPTER_PERMISSION_DENIED`, verify that the exact
service-account email was added and wait a minute or two before retrying. If it
returns `ADAPTER_NETWORK_NOT_FOUND`, the network code is likely wrong.
#### GAM buyer-routing default advertiser
For Google Ad Manager managed-sales-agent sources, Storefront can clear the
`Default GAM advertiser` setup blocker through the API. List cached advertiser
records with `GET /api/v2/storefront/esa/{esaId}/gam/advertisers`, or create
or find the intended catch-all advertiser with
`POST /api/v2/storefront/esa/{esaId}/gam/advertisers/ensure`. Then set the
tenant default with `PUT /api/v2/storefront/esa/{esaId}/gam/default-advertiser`
using the returned `advertiser.id`.
This flow configures the upstream sales-agent tenant directly. Operators do not
need to open the embedded sales-agent UI to set the default GAM advertiser.
Keep detailed GAM UI wording anchored to Google's own support documentation;
Apostra docs should describe the contract we own, the service-account email we
return, and the role/permission requirements we need.
Sources can't be deleted while their backing agent has non-terminal media buys (`ACTIVE`, `PAUSED`, `PENDING_APPROVAL`, or `INPUT_REQUIRED`). Cancel or terminate those first.
Payout details on file let Apostra settle payments on your behalf: Apostra collects from the buyer, deducts the configured fees, and pays you by bank transfer in your payout currency. Whether billing is required depends on the Seller Account execution path:
* **Optional** only for an official Apostra sales-adapter Seller Account on an existing downstream platform settlement agreement. A third-party sales agent or Agent-supplied (finished-product) source does not qualify.
* **Required to get paid** for every normal Seller Account, but never required to go live. Every normal Seller Account buy uses consolidated billing today (Apostra bills the buyer as your billing agent and passes each payment on once it arrives; you keep the buyer's credit risk); without payout details, funds still accrue against each booking, but Apostra has no way to disburse them. The readiness check `billing_setup` returns `isBlocker: false` in every state — it is advisory, not a go-live gate. An active source that explicitly lacks `agent` billing support still blocks readiness (a different check, `interchange_billing_support`). Separately, a Seller Account whose sales agents explicitly declare billing that leaves buyers no usable model is blocked by `supported_billing_declared`; an agent that declares no billing uses consolidated billing (see [Get readiness](/v2/storefront/tasks/get-readiness)).
If billing is **optional** and you skip it, only official sales-adapter buys on the existing downstream platform agreement can operate. Apostra does not bill or collect those media payments. Direct billing is not yet configurable for normal Seller Accounts.
Accounts under an organization inherit billing from the organization by default. A child-account administrator cannot set up or change a payout destination; a parent administrator can create a child-specific destination for a direct Seller Account.
#### 1. Save payout details
Enter your bank details in **Plan & Billing → Payouts**, or via the API:
```bash curl theme={null}
curl -X PUT https://api.apostra.com/api/v2/storefront/billing/payout-details \
-H "Authorization: Bearer scope3_..." \
-H "Content-Type: application/json" \
-d '{
"beneficiaryName": "Meridian Media Group Inc.",
"addressLine1": "500 Harbor Blvd",
"city": "Seattle",
"region": "WA",
"postalCode": "98101",
"countryCode": "US",
"accountNumber": "000123456789",
"bankIdentifierType": "FEDWIRE_ABA",
"bankIdentifierValue": "021000021",
"currency": "USD"
}'
```
`accountNumber` takes a bank account number or an IBAN; `bankIdentifierType` is one of `FEDWIRE_ABA`, `CHIPS_ABA`, `SWIFT_BIC`, or `BANK_CODE`, with the identifier itself in `bankIdentifierValue`. The account number is encrypted at the application layer before storage and used only to execute payouts; the account number is write-only and never displayed after save. See [Set payout details](/v2/storefront/billing/tasks/set-payout-details) for the full contract.
#### 2. Confirm what's on file
```bash curl theme={null}
curl https://api.apostra.com/api/v2/storefront/billing \
-H "Authorization: Bearer scope3_..."
```
```json Response theme={null}
{
"billing": {
"onboardingStatus": "complete",
"platformFeePercent": 12.5,
"currency": "USD",
"defaultNetDays": 30,
"payoutDetails": {
"beneficiaryName": "Meridian Media Group Inc.",
"accountNumberLast4": "6789",
"bankIdentifierType": "FEDWIRE_ABA",
"bankIdentifierValue": "021000021",
"completedAt": "2026-07-01T09:30:00Z"
},
"inherited": false
}
}
```
#### Other billing endpoints
| Endpoint | Purpose |
| - | - |
| `GET /api/v2/storefront/billing` | Fees, currency, net days, masked payout details |
| `PUT /api/v2/storefront/billing` | Admin-only — update fee config |
| `GET /api/v2/storefront/billing/accounts` | Organizations — billing status across the accounts under them |
Organization admins can pass `?targetCustomerId=` on billing endpoints to operate on a direct Seller Account's billing. Access is validated against the organization/account relationship before each call.
Apostra no longer uses Stripe for Seller Account payouts. Enter your bank details once (above) to keep consolidated billing on your media buys — details held by Stripe cannot be migrated.
A storefront has no stored `PENDING` or `ACTIVE` lifecycle state. Its effective
status is always derived from `isPaused`, archive state, and the current
readiness checks. Buyer agents can transact only when that projection is
`live`. Public marketplace discovery has one additional human-review gate:
transaction-ready Storefronts remain pending marketplace review until an Apostra
admin lists them.
#### 1. Confirm readiness
`GET /api/v2/storefront/readiness` computes every current requirement and the effective status. Call it any time during onboarding or operation to see what's missing.
```bash curl theme={null}
curl https://api.apostra.com/api/v2/storefront/readiness \
-H "Authorization: Bearer scope3_..."
```
```json Response theme={null}
{
"platformId": "acme-media",
"status": "ready",
"checks": [
{
"id": "inventory_sources",
"name": "Inventory sources",
"category": "inventory",
"description": "At least one source has been configured.",
"status": "complete",
"isBlocker": true,
"method": "agent",
"details": "1 source configured"
},
{
"id": "agent_status",
"name": "Inventory source status",
"category": "agents",
"description": "All inventory sources are active.",
"status": "complete",
"isBlocker": true,
"details": "1 of 1 inventory sources are active"
},
{
"id": "agent_auth",
"name": "Agent authentication",
"category": "agents",
"description": "All agents have been authenticated.",
"status": "complete",
"isBlocker": true,
"details": "1 of 1 agents are authenticated"
},
{
"id": "billing_setup",
"name": "Billing",
"category": "billing",
"description": "Payout details are on file.",
"status": "complete",
"isBlocker": false,
"details": "Payout details added"
}
]
}
```
Every check also carries a `requirement` classification so you always know what a checklist item actually demands of you:
* **`hard`** — must be resolved before the storefront can go live. Matches `isBlocker: true`.
* **`soft`** — advisory: improves outcomes but never blocks.
* **`platform_default`** — the platform applied a sensible default on your behalf; the `appliedDefault` field states the value in plain terms and how to change it. These are visibility items, never tasks.
The classification is path-aware: which checks appear, and whether each blocks,
depends on source treatment and the capability needed for that action. A
commercial plan choice does not hide setup checks or unrelated surfaces.
* **`publish_validation`** *(blocker; compatibility id)* — every active transaction path needs proof. A third-party Sales Agent Source reuses the canonical `media_buy_transaction` assertion from that Agent's exact current production revision; credentials, account mapping, reachability, and health remain Source-specific. In **Required to go live**, choose **Run transaction validation** to open the Test section for the exact Agent and inventory source that still needs proof. If an active Agent source is not yet linked to a registered Agent, Apostra opens **Source diagnostics** for that source so you can create or attach the Agent before retrying. If multiple active Agent connections match, select the connection to use or remove or reconcile the extra connections before retrying. You can also run the public transaction validation skill on that Source or ask the Agent owner to provide proof. A seller-owned no-spend sandbox test or successful live buyer media buy can still cover a Storefront-local path. An already-activated Source keeps its historical onboarding completion while current implementation health, Source health, and quarantine govern ongoing operation. Managed-only Storefronts report `complete` because Apostra operates that path.
* **`publisher_domains`** *(blocker)* — every storefront declares at least one publisher domain so buyers know what inventory is being sold. The operator domain identifies the company operating the storefront and may be different. `adagents.json` authorization is shown separately as the advisory `publisher_authorization` check and never blocks transactions.
* **`product_publisher_domains`** *(advisory during rollout)* — every active product should map to one of the storefront's declared publisher domains. Missing mappings and undeclared domains warn while older catalogs are backfilled; `adagents.json` authorization is separate and also advisory.
* **`approval_settings`** *(platform default)* — how buyer submissions are handled on Apostra-managed sources. Never a task; `appliedDefault` names the current posture and how to change it.
* **`inventory_sources`** *(blocker)* — at least one source must be connected. Buyer-facing `get_products` still requires at least one **active** source: Storefront-built storefronts need active ingredient sources, and Agent-supplied storefronts need an active source to proxy.
* **`agent_status`** *(blocker; compatibility id)* — every non-disabled external-agent inventory source must have canonical source status `ACTIVE`. No copied sidecar or legacy agent status can independently block it. This gates go-live; AAO compliance does **not** (see `agent_connectivity`).
* **`agent_auth`** *(blocker)* — non-OAuth agents must have a stored credential. OAuth agents are excluded once their token is captured.
* **`esa_go_live_certification`** *(blocker for booking only)* — a managed ad server can ask for a no-spend go-live certification before your first booking, such as the Google Ad Manager write canary or the FreeWheel booking certification. While one is open, buyers can still find your storefront, see its products and plan with them, but a booking is refused with a message saying you have not finished going live. Booking opens once the certification passes. Any other open ad-server setup blocker is reported as `esa_setup_tasks` and also keeps the storefront out of buyer discovery.
* **`agent_connectivity`** *(informational, surfaced on `GET /readiness/compliance`)* — reads each agent's AAO compliance verdict and returns per-agent track results and observations. A non-`passing` verdict surfaces as a prominent warning but does **not** block going live. See [Identity documents](/v2/concepts/identity-documents#blocks-vs-informs).
* **`billing_setup`** *(advisory, never a go-live blocker)* — payout details are **optional** only for the official Apostra sales-adapter compatibility path; the check returns `status: optional` with `isBlocker: false` and the external-agreements warning. They are **required to get paid** for every normal Seller Account, including third-party sales-agent and Agent-supplied (finished-product) sources — but not required to go live: the check returns `status: missing` with `isBlocker: false` until payout details are on file, and the Seller Account can activate and transact in the meantime. Funds accrue against every consolidated-billing booking either way; Apostra just can't disburse them until payout details are added. This projection is the same in non-production, so an account without payout details is never shown as billing-ready.
Top-level `status` is `blocked` if any check with `isBlocker: true` is not `complete`, otherwise `ready`. A check with `status: optional` is treated as not required.
#### Programmatic Agent validation
Use the exact Agent returned by V3 `get({ kind: "agent" })` and follow its
versioned `validationSkill`, or fetch the always-current skill directly from
its stable alias:
```text theme={null}
https://api.apostra.com/skills/test-sales-agent/SKILL.md
```
Choose an available profile from the skill's returned profile plan. A customer
account is offered the brief-only profile. The transaction profile is not
offered to customer accounts, and certification runs automatically on live
traffic. A request for the transaction profile from a customer account is
refused with `VALIDATION_ERROR` and reason `transaction_test_internal_only`;
it never runs a different test instead. Profiles shown as unavailable cannot be
started until they have an executable fixture and confirmation path. Do not
substitute Murph test wrappers, V2 `api_call`, or the retired standalone Test
Runs surface.
The brief-only profile uses public `save_connection` to enable the exact Seller
for its fresh sandbox Advertiser before discovery. Its reverse cleanup restores
that advertiser preference to `DEFAULT` before archiving the Advertiser; it
does not change account-level Seller selection.
After execution or a stop, read
`get({ kind: "agent", include: ["validationRuns", "diagnostics"] })` and the
exact Source diagnostics when Products were Source-attributed. A validation
run is evidence for the Agent revision it names and records its cleanup result;
it is not itself a certification claim.
For a deeper agent connectivity test (full AdCP compliance scenarios in sandbox mode), hit:
```bash curl theme={null}
curl https://api.apostra.com/api/v2/storefront/readiness/compliance \
-H "Authorization: Bearer scope3_..."
```
This may take up to 60 seconds.
#### 2. Go live or resume buyer intake
When every hard requirement is complete and the intake hold is still set, the
Seller Setup page shows **Go live** (or **Resume new business** for a storefront
that was live before). The action clears only the canonical intake hold and
then reloads readiness. The page does not present the storefront as live until
that write succeeds and the refreshed `canTransact` projection is true.
```bash curl theme={null}
curl -X PUT https://api.apostra.com/api/v2/storefront \
-H "Authorization: Bearer scope3_..." \
-H "Content-Type: application/json" \
-d '{ "isPaused": false }'
```
Clearing the canonical `isPaused` intake hold does not claim the storefront is ready. The write succeeds, and the response still projects `blocked` with the current failing checks until every hard requirement passes. The intake hold controls buyer discovery, new buys, and buyer edits; it does not pause or resume campaigns already delivering in your ad server.
```json Response theme={null}
{
"isPaused": false,
"canTransact": false,
"effectiveStatus": "blocked"
}
```
When the last hard requirement becomes complete, the same storefront without an intake hold projects `live` automatically. If a future requirement is added or current evidence fails, it projects `blocked` automatically without rewriting `isPaused` or an `adcp_agent` row.
#### 3. Marketplace review
The marketplace review state is independent from the derived storefront lifecycle:
| Marketplace state | Meaning |
| - | - |
| `PENDING_REVIEW` | Default for new Storefronts. The owning organization can configure and use the Storefront, but it is not shown in public buyer discovery. |
| `LISTED` | Reviewed and visible in public buyer discovery and marketplace browsing, subject to the normal live/source/credential gates. |
| `HIDDEN` | Intentionally removed from public buyer discovery, commonly for test, internal, or deprecated Storefronts. |
Apostra reviews newly live Storefronts before listing them. This keeps test
Storefronts and unreviewed sellers out of the broader marketplace without
blocking the operator's own setup work.
#### Lifecycle states
| Status | Meaning |
| - | - |
| `PENDING` | Created but not live. Buyer-side discovery does not surface it. |
| `ACTIVE` | Live. Buyer agents can transact; public discovery also requires marketplace review and listing. |
| `DISABLED` | Temporarily off. Existing media buys retain their current state and route, but new ones can't be placed. |
Allowed transitions: `PENDING ↔ ACTIVE ↔ DISABLED` (you can't go straight from `DISABLED` to `PENDING`).
***
## Troubleshooting
This is logged but not a blocker. The source is created and will auto-activate once its authentication requirements are configured. Reachability is reported independently; no action is required unless source health flips to `not-passing`.
This is an advisory warning, not a source-creation or activation blocker. Visit [agenticadvertising.org](https://agenticadvertising.org) and check the storyboard test results for your agent URL. Resolve the failing scenarios in your agent implementation so the registry verdict improves and marketplace review has a clean signal.
The agent's `endpointUrl` doesn't appear in the AAO registry at all. Register it through the AAO operator dashboard before retrying.
Your agent record is `PENDING`. Most often this means the auth credential hasn't been verified yet. Re-submit the source with a fresh `auth` block, or for OAuth agents make sure the OAuth callback completed.
A non-OAuth agent has no stored credential. `PUT /api/v2/storefront/inventory-sources/:sourceId` with an `auth` block to set one.
Look at the `compliance` array on the check — each entry has per-track `failureReason`, `summary`, and `observations`. This check is informational for activation, but it is still useful debugging signal. The most common causes are auth misconfiguration, schema drift between your agent and the AdCP spec, and agent-side timeouts beyond 60s.
Matching the registered `customerDomain` is not enough by itself — the account domain must also be approved by active-member email ownership or Apostra admin attestation. Same-value `operatorDomain` updates preserve the current verification state; have an Apostra admin approve the account domain or change the storefront to the correct operator domain.
An account inherits its organization's payout configuration. Only a parent
organization admin can create or change a direct child Seller Account's destination.
In **Plan & Billing → Payouts**, select the child Seller Account; through REST,
pass `?targetCustomerId=`. A child-only admin cannot create an
account-specific payout destination.
***
## Next steps
* [Prepare inventory source inputs](/v2/setup/publisher-onboarding-starter-kit) — when a source needs inputs supplied separately, identify where avails, products, CRM context, creative formats, properties, execution, and reporting come from; a complete external-agent setup can skip it
* [Storefront API Reference](/v2/storefront-api-reference) — full endpoint reference for storefront, billing, and inventory source endpoints
* [Authentication](/v2/authentication) — API key and OAuth flows
* [Storefront object guide](/v2/object-guides/storefront) — how buyer agents see your storefront once it's live (discovery, credentials, sources)
# What you need for a GAM pilot
Source: https://docs.apostra.com/v2/setup/storefront-pilot
The short checklist for testing a publisher Seller Account connected to Google Ad Manager
A good pilot should show that the Merchandising Agent can answer the kinds of
briefs the publisher wants to win, not just prove that a campaign can reach the
ad server.
Keep the execution focused: use one ad server, one or two buyers, a handful of
real brief–response pairs, and one no-spend campaign. Give the Merchandising
Agent enough inventory, signals, pricing, and selling guidance to make those
briefs interesting.
## What the publisher needs
* **A verified Seller Account** with the publisher's name and domain.
* **A Google Ad Manager connection.** Add Apostra service account as a GAM
user with the access needed to read inventory and traffic the test campaign.
* **A separate sandbox advertiser.** Either create or designate one, assign it
to Apostra service account, and map it for the pilot; or let Apostra create
**Apostra - Sandbox** by granting the service account advertiser-create
permission. The no-spend test never uses a live or default advertiser.
* **Representative inventory.** Include the properties, placements, formats,
and channels needed for the briefs the publisher wants to test. Do not limit
the pilot to a generic run-of-site package if that is not what the publisher
wants to sell.
* **Useful signals.** Turn the audience segments and contextual targeting the
publisher already uses in GAM into signals the Merchandising Agent can use.
Include enough to show how the offer changes for different briefs.
* **Useful sales material.** Give the Merchandising Agent enough to explain who
the publisher reaches, why its properties and editorial contexts matter,
which formats or sponsorships are distinctive, and what proof supports those
claims. A media kit, audience cards, and case studies are more useful than a
media-kit file on its own.
* **Pricing it can apply.** Set the currency and the price guidance that matters
for the pilot: target prices or floors, when each price applies, and any
audience, context, format, or sponsorship premium—and any minimum-spend rule—the
test briefs should trigger. One generic CPM is not enough if the offers are
meant to be different.
* **Basic selling choices.** Say which advertisers are not accepted, which
creative formats are allowed, and whether the publisher wants to approve the
brief or creative before trafficking.
* **Real brief–response pairs.** Supply three to five recent briefs the publisher
actually received, together with the proposal or response it sent for each
one. Choose examples that show different demand—for example broad reach, a
valuable audience, contextual relevance, or a distinctive sponsorship—and a
declined or no-fit brief if one is available. The examples may be sanitized,
but keep the objective, audience or context, market, flight, budget, important
constraints, recommended plan, price logic, and explanation of why it fit.
Mark any pricing or availability that is no longer current. Use made-up briefs
only when real examples are not available.
* **One test buyer.** Decide who will participate. Apostra routes the no-spend
campaign through the separate sandbox advertiser once one of the setup paths
above is complete.
* **A billing choice.** Decide whether Apostra or the publisher will bill
the buyer. If the publisher bills directly, an administrator for an existing
buyer-owned account selects and approves direct billing on its Seller
connection before a paid campaign. A sponsored buyer does not approve or
change billing separately.
* **Discovery plus one executed test.** Check that the Merchandising Agent can
return relevant, differentiated products for each agreed brief. Then take one
proposal through campaign creation, creative review, GAM trafficking, and a
simple delivery report.
The publisher connects GAM to its Seller Account, and its Merchandising Agent
builds and sells products from that inventory. The publisher can still invoice
the buyer directly. That changes who sends the invoice, not how inventory is
connected or merchandised.
## Read the response like a buyer
Run the real briefs again and compare the new response with what the publisher
actually sent. Ask five simple questions:
* Does it reflect the buyer's objective and constraints?
* Does it make a specific case for this publisher using relevant audience,
context, selling points, or proof—not generic reach language?
* Is the plan coherent, with the right products, formats, price, currency,
flight, and budget?
* Does it clearly state any missing capability, unsupported claim, or reason not
to answer the brief?
* Could a seller send it after a quick factual review, rather than rewrite it?
If not, fix the missing inventory, signal, price guidance, or selling material
and run the same brief again.
## Only add these if the pilot uses them
* **AXE or emissions-specific delivery:** configure the additional GAM keys
needed for that path when it is part of the pilot. Ordinary audience and
contextual signals belong in the core pilot setup above.
* **Public buyer discovery:** complete the extra review and authorization work
needed for a public marketplace listing. A named-buyer pilot can start
privately.
* **Live GAM advertiser routing:** for paid activity, configure the live default
advertiser or map the buyer to a dedicated advertiser. Neither is used by the
no-spend sandbox campaign.
* **Paid delivery:** confirm the direct-billed buyer can be invoiced, or finish
Apostra billing setup, before moving from no-spend testing to paid media.
## What Apostra helps with
Apostra connects GAM to the Seller Account, turns the selected inventory into
buyer-visible products, routes the sandbox test, provides the buyer-facing
agent, and helps run the test from brief to report. The publisher supplies the
inventory choices, commercial terms, creative rules, GAM access, sandbox setup
or create permission, and approvals.
## The pilot is complete when
* The Merchandising Agent produces relevant, differentiated products for the
agreed briefs—not the same generic package every time.
* The products use the expected inventory, signals, formats, and pricing.
* The proposal answers the buyer's objective, explains why the plan fits, gives
clear terms, and does not invent support the publisher has not supplied.
* The no-spend campaign and creative reach GAM under the **Apostra -
Sandbox** advertiser.
* The publisher's approval choices are followed.
* Delivery can be read back in a simple report.
For the full production setup, use [Seller Account onboarding](/v2/setup/storefront-onboarding).
For a custom inventory feed or less-structured source, use the
[publisher onboarding starter kit](/v2/setup/publisher-onboarding-starter-kit).
For the two sandbox-advertiser setup paths, see
[Sandbox mode](/v2/features/sandbox#seller-ad-server-setup).
# One storefront. Every buyer. Every channel.
Source: https://docs.apostra.com/v2/storefront-introduction
A technical introduction to running a storefront on Apostra — for the publishers, SSPs, and retail media networks wiring the next twenty years of inventory.
> Coming from the [Apostra overview](/v2/introduction)? This is the storefront-side deep dive — what you expose, in the order you'll expose it.
"Sellers" here means sellers of media — publishers, SSPs, retail media networks.
Register a storefront, wire an inventory source, accept your first media buy.
OAuth into Claude or ChatGPT and operate the storefront from any agent.
## You've integrated this market before.
If you've sold inventory programmatically in the last decade, you've taken the same meeting fifty times. Each demand source wants its own deal-id flow, its own creative spec, its own reporting cadence, its own brand-safety vendor in the loop. You've staffed an integrations team to keep up. The team is the bottleneck.
That's not a yield problem. That's a transaction-cost problem.
## The shift.
Programmatic exposes a **bid request firehose**: rate-card every impression, evaluate every QPS spike, optimize floor prices, manage daisy chains.
Agentic exposes a **product catalog**: list what you sell, describe who it reaches, accept a brief, negotiate, deliver.
You stop being a destination for bid requests. You start being a discoverable inventory catalog that buyer agents can find, query, and transact with directly.
> Programmatic asks: what is this impression worth?
> Agentic asks: which products match this brief?
> One is an auction. The other is a sales conversation — at machine speed, at any deal size.
## What you actually integrate.
You expose a **Storefront** on Apostra. The Storefront is your
buyer-facing home; your Merchandising Agent runs it, responds to buyer briefs,
and handles the AdCP media-buy workflow. Two surfaces, one product:
* **MCP** — three tools (`health`, `ask_about_capability`, `api_call`) drive the full Apostra from any MCP-speaking model. Manage inventory sources, products, and deals from any agent surface.
* **REST + OpenAPI** — for CLIs, servers, and your existing inventory systems. OAuth for agent connectors, API keys for servers.
A Storefront connects one or more **inventory sources**. An inventory source can be an external sales agent you already run, one of your ad servers with Apostra-managed sales-agent plumbing behind it, or another Storefront. Your Merchandising Agent speaks **AdCP**, the open protocol for agentic media buying. That's how every buyer agent on Apostra can transact with you on day one — not just the ones you've signed paper with.
An external sales-agent source supplies products along one of two paths, and
the paths are independent of your account tier. On the **Agent-supplied**
path, buyer briefs flow to your sales agent and it answers live — this works
on any account, including one with Listing + Distribution when it keeps routing briefs
through its own agent. On the **Storefront-built** path, Apostra
composes products from cached **components** your source exposes; this
requires a separate Merchandising entitlement *and* a source that supports component
caching. Successful caching means Apostra can read your raw products as
stable components — ids, pricing, formats, publisher properties or selectors,
delivery type, and execution metadata — without calling your source again
while composing a buyer proposal. If your source does not support component
caching yet, the Agent-supplied path still works; Murph reports cache health
separately from live source-call health.
AdCP moves the transaction. Apostra handles the layers the protocol can't — contracts, billing, multi-agent aggregation, governance.
## Marketplace review.
Going live and appearing in the broader buyer marketplace are separate gates.
New Storefronts start pending marketplace review. Once the Storefront opens for
transactions, Apostra reviews it before listing it for public buyer discovery.
While review is pending, the operator can keep configuring the Storefront and
work with known partners, but buyers browsing Apostra will not see it.
After approval, admins list the Storefront; if a test or internal Storefront
should not be public, admins can keep it hidden.
## Where this leaves you.
One storefront. Every buyer agent in the marketplace. Including the buyers who would never have built a bespoke integration with you specifically — small advertisers, niche brands, public-interest campaigns, the long tail of demand that the existing system makes too expensive to serve.
When the buyer agent's transaction cost approaches zero, the deals that economics previously made impossible become trivial. A $500 podcast sponsorship. A $2,000 local news campaign. A \$200 placement during a community event. These were always worth running. They just weren't worth the overhead. They are now.
That's the change. The rest of this site explains how to ship it.
Register your storefront, expose inventory, accept buyer demand.
Every REST endpoint with request/response schemas.
The conceptual model behind storefronts, signals, and agent-first design.
The open protocol Apostra speaks. Apache 2.0.
# Storefront next actions
Source: https://docs.apostra.com/v2/storefront/nudges/overview
Actionable recommendations from your storefront status about setup and source health.
Storefront next actions are in beta. The checks and recommendations may evolve
before general availability.
A **next action** is a concrete step returned by your storefront's status when
something needs your attention — either a required step that blocks availability
or a recommended step that improves readiness.
Ask Murph for your storefront status to see the current list. The status response
is the source of truth: it derives each action from the same readiness and source
health checks used by the storefront, instead of maintaining a separate reminder
queue.
The former Storefront Brain nudge queue and its automatic in-app, email, and Slack
nudges are retired. Check your storefront status in Murph for current next actions.
Existing source-health alerts remain separate and continue to follow your
notification preferences.
## What produces a next action
Next actions are derived from setup and health signals such as:
* **A sales agent needs attention.** Source diagnostics explain what is failing,
who can fix it, and the appropriate next step.
* **Setup is incomplete.** For example, a storefront missing its default
settlement currency can't go live — status tells you what's left to do.
Informational checks with no concrete action are not included in the next-action
list.
## Where you see them
Next actions appear in the V3 storefront status response in Murph. Each action is
marked as either **required** or **recommended** and links to the relevant tool or
page when one is available.
They are not sent through a separate always-on email, Slack, or notification
ladder. Existing source-health alerts remain independent and follow their own
notification preferences.
## Related
Check the health of an inventory source your status flagged.
The audit feed of what's happened on your storefront.
# Playbook instructions
Source: https://docs.apostra.com/v2/storefront/operating-instructions/overview
The versioned Playbook guidance your Merchandising Agent applies when composing products
Playbook instructions are the versioned guidance for your storefront — a Markdown body the Merchandising Agent consumes when it composes products for buyers. Each save creates a new immutable version; exactly one version is **active** at a time, and only the active version influences compositions. You author a new version, review it, then activate it to swap the storefront's active pointer.
## What belongs here
Use operating instructions for product packaging, naming, selection, and explanation. Each related fact has one writable home:
| Fact | Where to configure it |
| - | - |
| Structured prices, floors, and currencies | **Playbook pricing** |
| Brand/operator discounts and scoped notes | **Buyer Discounts** and **Buyer Instructions** |
| Buyer-visible channels and accepted countries | **Listing** |
| Advertiser, category, and creative eligibility | **AI Business Rules** |
| Product packaging, naming, selection, and explanation | **Playbook instructions** |
New versions that repeat facts owned by another field are rejected. Older immutable versions remain readable and return `ownershipIssues` when a conflict is detected. A flagged older version cannot be reactivated; create a cleaned-up version instead. If a flagged version is already active, the conflicting lines are omitted from the Merchandising Agent's runtime copy while the immutable saved version remains visible for cleanup. Structured Playbook pricing, Buyer Discounts, Buyer Instructions, the Listing, and AI Business Rules remain authoritative. A Product Marketing Media Kit is merchandising source material, not the owner of buyer-visible market configuration.
Versions are numbered per storefront. The first version is `1`, and numbers are monotone and never reused. Activating a version does not delete older ones — they stay in history so you can re-read or re-activate a prior version.
All examples use the storefront base URL:
```
https://api.apostra.com/api/v2/storefront
```
Authenticate every request with `Authorization: Bearer $SCOPE3_API_KEY`.
## Deciding whether to answer at all
Before the Merchandising Agent composes anything, it decides whether this brief
is yours to answer: pitch it in full, counter-pitch it with a credible reframe,
or pass with a short branded decline. Those thresholds are your **selling
doctrine**, and they ride these same versions — one save, one activation, one
history.
See [Selling doctrine](/v2/storefront/operating-instructions/selling-doctrine)
for the settings, the named starting points, and how to test a change before
you activate it.
## Buyers do not see your rules
Your operating instructions are confidential to you. They steer how the Merchandising Agent composes, but they are never sent to a buyer or a buyer's agent, and everything the agent does send a buyer is written in market terms rather than by quoting your rulebook.
That splits the explanation of *why* a proposal looks the way it does into two audiences:
| Audience | What they get | Where |
| - | - | - |
| **You and your team** | The candid account: which rule applied, which inventory or signal was excluded and why, which component and signal ids were involved | The intelligence run for the request, its decision record, and Murph's brief tests |
| **The buyer's agent** | The proposal in market terms — what is being offered, how it fits the brief, what is not part of it | The `get_products` response |
So a rule that keeps video out of a proposal reaches the buyer as "video is not part of this proposal", never as your rule text. The same separation covers component and product ids, signal-vendor names, and setup vocabulary: they are yours, and they stay on your side of the conversation.
### Telling buyers why a brief was declined
When a brief matches none of your inventory, whether the buyer hears *why* is your call — it is a negotiation decision, not a platform default. "Your budget is below what we can sell this inventory for" invites a better bid; sometimes you want that, sometimes you would rather say nothing.
By default your storefront says nothing: the buyer is told that no products matched and how to broaden the brief. Authorize a fuller answer by writing the rule here, in your own terms — for example:
```markdown theme={null}
When we decline a brief because the budget is below our floor, tell the buyer
their CPM is short of what we can sell this inventory for and invite a higher
bid. Do not name the floor.
When we decline because we don't have the channel they asked for, say which
channels we do have.
```
Your Merchandising Agent then writes the decline in your voice, within what you authorized. Three limits are mechanical: a decline that quotes your rulebook, names a component or signal id, or runs long is replaced with the neutral answer, and the attempt is flagged to us. Beyond those, the agent is instructed to keep signal sources and setup vocabulary out of anything a buyer reads. Say nothing about what buyers should hear and nothing is disclosed.
To see which of the two a buyer would get, run the brief through Murph — a brief test reports the decline the buyer would receive, the candid internal reason, and whether the wording came from your rules or the neutral fallback.
## Task reference
`GET /operating-instructions` — every version, newest first
`POST /operating-instructions` — author a new version
`GET /operating-instructions/active` — the version in force now
`GET /operating-instructions/{version}` — one version by number
`POST /operating-instructions/{version}/activate` — swap the active pointer
Pitch, counter-pitch, or pass — the qualification judgment on the same versions
# Storefront
Source: https://docs.apostra.com/v2/storefront/overview
What a storefront is for an operator — inventory sources, business profile, operating instructions, readiness, and marketplace listing.
## Overview
Your **storefront** is your buyer-facing home on Apostra: the
business presence, name, and discovery surface buyers use to understand who
they are buying from. Behind it, the Merchandising Agent answers
buyer briefs, composes products, and runs the AdCP media-buy workflow on your
behalf, drawing from one or more **inventory sources** you connect.
This page is the operator's map of the storefront object. Each domain below has
its own task pages; this overview explains what the pieces are and how they fit
together.
**Merchandising is open beta.** Every media company can use it — there is no
cohort to be admitted into and nothing to apply for. It is beta rather than
generally available because the journey is newly built and we have not yet
watched enough publishers through it to promise it won't break. The
capabilities inside it keep their own maturity, and any step a person does by
hand is marked as such rather than described as automatic.
**Running real-money campaigns on your own inventory is limited to named
companies.** The no-spend sandbox — build, activate and read a campaign
against your own storefront — is open to every media company. The same
workflow with live spend is switched on for named companies while it is
proven, and for those companies live campaigns are read-only in the campaigns
experience; changes go through the agent rather than a native write path.
## Seller navigation
Before your storefront goes live, each inventory source in the rail shows its
setup state. A source that is waiting on you names the step to take: **Add
products**, **Add credentials**, or **Add product discovery support**. It does
not show progress, because nothing is running until you act. A source partway
through catalog mapping shows how many items are mapped, for example 4/6. Once
the storefront is live, these setup states retire and the rail shows only
problems that need attention.
The **Advertisers** tab opens on your sole visible advertiser automatically.
When you have several, it reopens the advertiser you last selected for this
account; when there is no earlier choice (or no advertisers), it shows the
account name and advertiser count. This choice survives a reload and is kept
separately for each person and account. **Sandbox** remains a toggle-revealed
badge, not a lens, and the visible set after that toggle determines the
default. A link that already names an advertiser always takes precedence.
The rail has a fixed picker with that advertiser's class, recent selections,
search, and **All advertisers** to return to the overview. Below it are only that advertiser's
jobs. A House or Self-serve advertiser offers **Overview**, **Campaigns**,
**Creatives**, and **Reporting**. Its Overview opens that advertiser's scoped
Campaigns Page. An External advertiser with a resolved relationship offers
**Overview**, **Media buys**, **Creatives**, and **Account**; its Overview
opens that relationship's Account setup case. An unresolved External row does
not offer Overview. A System advertiser offers read-only **Overview**, opening
that System row's read-only roster detail. Overview always keeps the selected
advertiser scope; only **All advertisers** returns you to the full roster.
When you select an External or System advertiser, its filtered roster detail
stays in the canvas until you choose one of that advertiser's available jobs;
you will not be taken to Campaigns.
Seller analytics is still available from **Operate → Dashboard**.
Each roster row carries one of four classes — House, Self-serve, External, or
System — shown as a small leading icon. House and Self-serve are advertisers
you operate (Self-serve means a client runs their own workflow inside your
roster, but you can still edit); External is any counterparty reached through
an AdCP account relationship. System is an Apostra-operated reviewer or
compliance-canary advertiser. It is always read-only to the seller and takes
precedence over every other class. The class comes from its provisioning
record, never its name, and it does not use the managed-marketplace class
label.
To see your buyers, open **Buyers**. Each external counterparty appears there,
and a pending setup case shows **awaiting you** in the **Needs you** signal.
A House or Self-serve row opens **Sellers, Activity, Campaigns, Creatives,
Catalogs, Conversions, and Reporting** —
there is no Account tab for these, since there's no relationship or setup
case behind an advertiser you operate. An External row opens **Media buys** by
default: the buyer's approval queue and active buys with you, including their
synced creative state. Its sublist also offers **Creatives** and **Account**;
it never opens the buyer's own
campaign container. The roster shows the relationship's buy count and most
recent activity when that relationship has routed activity.
The **Account** tab is where the buyer's billing path, setup requests, and
inventory mappings live for that relationship — one place instead of two.
System rows expose no mutation or navigation affordance. The `shared_default`
billing binding is a billing route, not an advertiser, and is not a seller
roster row.
External rows show neutral buyer account-setup status when action remains:
**Needs setup — waiting on you**, **Waiting on buyer**, or **Awaiting
billing**. The **Needs you** row counts only advertisers with a case waiting
on you and filters the canvas to those rows, regardless of lens; there is no
separate inbound-requests list. Selecting a row awaiting your decision opens
its **Account** job's setup case — the same case available from Buyer Account
Mapping. Buyer account setup does not change storefront health or the
Inventory tab's go-live status. See
[Buyer Account Mapping](/v2/storefront/buyer-account-mapping#account-setup-in-advertisers)
for each state, who acts, and the case action; House, Self-serve, and System
rows do not carry a setup state.
```
Storefront (your buyer-facing surface)
├── Business Profile (identity, positioning, authorized domains)
├── Discovery (brand-derived card and publication visibility)
├── Operating Instructions (versioned guidance; exactly one active)
└── Inventory Source(s) (AdCP agent — discovery + execution)
└── Buyer accounts (the buyers that transact through the source)
```
All examples use the storefront base URL and a bearer token:
```
https://api.apostra.com/api/v2/storefront
```
```bash theme={null}
curl https://api.apostra.com/api/v2/storefront/readiness \
-H "Authorization: Bearer $SCOPE3_API_KEY"
```
## Discovery publication
The listing (previously called the media kit or Discovery Card) is the storefront's
canonical buyer-visible identity and coverage summary. Seller admins publish it from **Storefront settings →
Discovery** by choosing a visibility and selecting **Publish**. Name,
description, website, logo, colors, and generated icons are derived from the
storefront card and its `brand.json` profile, so there is no second directory
profile to maintain.
Customer 1 can also activate the platform-managed public origin at
`api.apostra.com`; other customers remain limited to private or Apostra
visibility during the initial rollout. OpenAI verification is optional and
appears only after that public origin is active. See
[Publisher domains](/v2/storefront/publisher-domains#discovery-publication-origins)
for public routes, verification, and withdrawal behavior.
## Inventory sources
An **inventory source** is private upstream supply behind your storefront. It
can be an AdCP-compatible agent, an ad-server connection, a runtime link to
another storefront, or a modular lifecycle. Each path supports the product
discovery (`get_products`) and media-buy execution (`create_media_buy`) stages
it advertises. A storefront can use multiple sources, and the products buyers
see are assembled from the supply they provide.
Sources come in a few shapes:
* **External sales agents** you already run, connected by endpoint and auth.
* **Managed ad servers** — Google Ad Manager, FreeWheel, SpringServe, or AdsWizz —
where Apostra runs the sales-agent plumbing in front of your ad server.
* **Other storefronts** that have authorized passthrough or federation.
* **Modular sources** assembled from catalog/avails, booking, creative,
execution, reporting, and human-work modules.
Use [Choose how your storefront gets supply](/v2/storefront/inventory-sources/choosing-a-source)
before connecting anything. It also explains the separate storefront-adapter
path, which configures the storefront dispatcher rather than adding an
inventory source.
Each source declares its capabilities (`products`, `createMediaBuy`,
`updateMediaBuy`, `signals`, `wholesaleProducts`) and whether it requires
authentication. A Storefront-built storefront assembles products across active
sources; an Agent-supplied storefront proxies `get_products` to one upstream
source and overlays your storefront identity and buyer instructions on the
response.
## Business profile
Your **business profile** is captured during storefront setup and helps the
Merchandising Agent describe your business, follow the right merchandising
guidance, and understand which publisher domains it is authorized to sell
through the storefront.
| Field | Type | Description |
| - | - | - |
| `summary` | string | Concise explanation of what the storefront sells. Maximum 2000 characters. |
| `agentName` | string | Deprecated agent-name field, kept for legacy compatibility only. Prefer the storefront display name for buyer-facing naming. Maximum 80 characters. |
| `agentPersonality` | string | **Deprecated.** An agent has no personality of its own — voice belongs to the brand and is read from the brand manifest (`tone`). Nothing consumes this field. Maximum 1000 characters. |
| `propertyCount` | integer | Approximate number of owned publisher properties, from 0 to 10,000. |
| `channels` | string\[] | The full AdCP MediaChannel vocabulary the storefront offers: `display`, `olv`, `social`, `search`, `ctv`, `linear_tv`, `radio`, `streaming_audio`, `podcast`, `dooh`, `ooh`, `print`, `cinema`, `email`, `gaming`, `retail_media`, `influencer`, `affiliate`, `product_placement`, or `sponsored_intelligence`. The legacy aliases `audio` (for `streaming_audio`) and `video` (for `olv`) are also accepted on write and round-trip unchanged. |
| `regions` | string\[] | Up to 64 ISO alpha-2 country codes or region groups such as `EMEA` and `WORLDWIDE`. |
| `verticals` | string\[] | Up to 32 audience or content focus areas, each no more than 80 characters. |
| `publisherDomains` | string\[] | Publisher domains or properties the storefront is authorized to sell. Up to 128 valid domains; each is normalized to lowercase, deduplicated, and limited to 253 characters. |
| `evidenceUrls` | string\[] | Up to 16 media-kit, about-page, or other source URLs. |
| `notes` | string | Confirmed context that does not fit the structured fields. Maximum 4000 characters. |
| `updatedBy` | string | Audit marker for the last writer. The Business Profile page manages this automatically. |
`businessProfile.regions` is the canonical market configuration used by storefront qualification and cards. The legacy top-level `regions` field remains a compatibility alias: writing it updates the Business Profile, and reads project the Business Profile value so the two surfaces cannot disagree.
```json Example theme={null}
{
"businessProfile": {
"summary": "Premium sports inventory across web, mobile, and CTV.",
"channels": ["display", "ctv"],
"regions": ["GB", "EMEA"],
"publisherDomains": ["touchline.example", "football.touchline.example"]
}
}
```
Omitting `publisherDomains` means the authorized domains are unknown or have
not been captured yet. It does not mean the storefront is authorized to sell
every domain.
In an MCP-compatible assistant, call `get_business_profile` to open the
portable Business Profile page. Operators can review, create, and edit the
profile in the page. An assistant can also call `update_business_profile` for
an exact change confirmed in conversation. That tool applies a patch: omitted
fields from the profile state read for that call stay unchanged, while an
explicit `null` clears a field. Overlapping patch calls use last-writer-wins
behavior, so clients should serialize concurrent edits.
For managed sales-agent storefronts, this field is the authoritative
publisher-domain set Apostra syncs to the managed sales agent. See
[Publisher domains](/v2/storefront/publisher-domains) for replacement and
removal behavior.
## Operating instructions
**Operating instructions** are versioned guidance that steers how your
storefront responds to demand — what to package, how to position, and how to
negotiate. Exactly one version is active at a time; publishing a new version
supersedes the prior one without losing history.
Operating instructions are authoritative over historical analytics signals.
When the Merchandising Agent composes products, your written instructions and
[buyer instructions](/v2/storefront/buyer-instructions/overview) win over any
directional signal derived from recent outcomes.
## Readiness diagnostics
`GET /readiness` tells you whether your storefront can transact now. It returns
blocking checks for inventory sources, agent status, and agent auth, plus a
per-source `sourceDiagnostics[]` array used by setup surfaces. The overall
`status` is `ready` or `blocked`; each check reports `complete`, `partial`,
`missing`, or `optional`, and carries an `isBlocker` flag.
Storefront lifecycle is not stored as a state. The seller-owned control is the
buyer **intake hold**, exposed under the released compatibility name
`isPaused`. It defaults to `false`. Setting it to `true` removes products from
buyer discovery and blocks new media buys and buyer edits. Seller preview still
works, and an approved buy that has not yet been sent to a source waits in the
queue and forwards automatically after intake resumes.
Clearing an intake hold is always available unless the storefront is archived:
it restores the seller's prior neutral intake posture without claiming that a
new readiness, runtime-safety, or administrative requirement has passed.
Those independent safeguards continue to determine whether new business can
actually transact.
An intake hold does **not** pause campaigns already delivering in your ad
server. Existing campaigns keep delivering and reporting. A storefront-wide
delivery freeze is a separate, confirmed operation because resuming it must not
activate campaigns that were already paused before the freeze.
Effective status remains the v2-compatible `archived`, `paused`, `blocked`, or
`live`. A storefront without an intake hold becomes live automatically when
every current requirement passes, and becomes blocked automatically when any
new or existing requirement fails. The deprecated `transacting` field is only
an API compatibility projection of `!isPaused`; it is not proof of readiness.
See [Get readiness](/v2/storefront/tasks/get-readiness) for the full field
reference.
## Marketplace review and AAO signals
New storefronts start in a pending-review marketplace state. You can configure
and use your storefront, and open it for known transactions, but public buyer
discovery only includes storefronts that Apostra has reviewed and listed.
Marketplace listing uses signals from the
[Agentic Advertising Organization](https://agenticadvertising.org) (AAO)
registry. Source registration, storyboard compliance, and publisher
authorization are different checks: registration blocks connecting an external
agent source; compliance is advisory for activation; publisher `adagents.json`
authorization is surfaced as an advisory setup/product signal today. Apostra's
marketplace review decides when a live storefront is listed in public buyer discovery. Use
[Discover agents](/v2/storefront/tasks/discover-agents) and
[Resolve brand](/v2/storefront/tasks/resolve-brand) to inspect what the registry
knows about your brand domain and the brands you transact with.
An admin can hide a listed storefront, which removes it from public buyer
discovery without deleting the storefront or its inventory sources.
## Storefront domains
Connect and manage the AdCP agents and ad servers behind your storefront.
Versioned merchandising and negotiation guidance.
The targeting and optimization inputs your sources expose.
Review and decide on incoming media buys before they go live.
Payout bank details and fees so Apostra can pay you.
Win rate, buyer asks, top products, and negotiation posture.
Per-buyer discounts, notes, and country scoping.
# Playbook selling guidance and rate cards
Source: https://docs.apostra.com/v2/storefront/playbook/overview
The Playbook shows the current read-only rate card while the Library owns rate-card editing and version history.
## Overview
**Rate cards live in the Library.** The Playbook shows the current rate card
read-only. Use **Edit in Library** to open its typed **Rate card** document and
make changes there.
**Playbook pricing** is the set of pricing facts your Seller Account gives the
[Merchandising Agent](/v2/concepts/storefront-agent) so it prices products the
way you would. Instead of a static rate card returned to every buyer, you hand
over the *anchors* — "rich media runs around a \$30 CPM", "never quote CTV below
\$22", "holiday inventory carries a premium" — and the agent applies them as it
composes a proposal for the buyer in front of it.
A pricing fact is deliberately **lightly structured**: a short label, a
natural-language condition describing when it applies, and a price anchor (target,
floor, and/or ceiling). It is reasoning context for the agent, not a rigid product
template. This is where the numbers from your rate card, media kit, or a setup-call
conversation live.
These are **your** product-pricing facts, separate from any Apostra contract rate
card. They price the inventory you sell through your Seller Account.
## What the Playbook needs from you
Give the Merchandising Agent the durable inputs it needs to pitch your
inventory consistently. Start with the required inputs, then add the
recommended evidence that makes its proposals more specific.
### Required
* **Business rules — what you accept.** Your acceptance policy tells the agent
which briefs and buyers are on policy, so it can decline demand that does not
fit your business.
* **Rate card / pricing anchors — how you price.** Floors, targets, ceilings,
and the conditions that apply to them give the agent a defensible price for
each discovery response.
### Recommended
* **Media kit — how you're represented.** Your listing and business profile
give the agent the seller facts it needs to represent your inventory clearly.
* **Selling guidance — how you package and select.** Versioned Playbook
guidance tells the agent how to name, package, and choose products for a
brief.
* **Past proposals and sales decks — what wins.** Product Marketing material
gives the agent examples of your proven positioning and proposal language.
Each input sharpens how the agent responds to a buyer brief; you can add or
update it over time as your inventory and sales strategy change.
The Playbook keeps selling guidance alongside a read-only rate-card summary.
The Library owns rate-card editing and its version history.
## What a rate card holds
The portable **Playbook** Page lets you review structured pricing alongside
versioned selling guidance in any MCP Apps host. Edit prices in the linked
Library **Rate card** document. The same configuration remains available through
the legacy v2 Seller Account API, in its
`compositionPricing` field —
a `StorefrontCompositionPricing` object:
| Field | Type | Description |
| - | - | - |
| `rateCard.currency` | ISO 4217 string | Default currency for the facts below (e.g. `USD`) |
| `rateCard.facts` | array | Up to 500 pricing facts (below) |
| `targetPercentile` | `p50` \| `p75` \| `p90` | **Deprecated.** Historical clearing percentile, retained for explainability only — it no longer sets prices. Pricing is value-led, bounded by your floors and ceilings |
Each **pricing fact** (`StorefrontPricingFact`):
| Field | Type | Description |
| - | - | - |
| `id` | string | Stable operator-facing id. Preserved across rewrites when the fact means the same thing |
| `label` | string | Short display label (e.g. "Rich media packages") |
| `appliesWhen` | string | Natural-language condition. Intentionally not a rigid template — the agent uses it as reasoning context |
| `pricing.targetPrice` | number | The price you'd like to land at |
| `pricing.floorPrice` | number | The lowest you'll quote. A quote is never composed below it |
| `pricing.ceilingPrice` | number | The most you'd quote (must be ≥ `floorPrice`) |
| `strength` | `hard_floor` \| `default` \| `guidance` | `hard_floor` is a must-not-go-below constraint; `default` is the preferred pricing anchor; `guidance` is advisory context only |
| `hints` | object | Optional structured hints for deterministic matching. Missing hints are fine |
| `hints.formatDimensions` | `{ width, height }[]` | Creative sizes this fact applies to. A pair matches when a selected canonical option equals it, lists it in `sizes[]`, or includes it within min/max bounds |
| `provenance` | string | Where the fact came from (e.g. "Uploaded 2026 rate card, page 3") |
| `notes` | string | Rationale or caveats for operators and Murph |
Every fact must carry at least one of `targetPrice` or `floorPrice`.
For a size-specific rate-card line, set `hints.formatDimensions` rather than
relying on words in the label. For example, a 300×250 fact uses
`"formatDimensions": [{ "width": 300, "height": 250 }]`. When one composed
product selects several sizes, every fact whose dimensions are selected can
apply; the highest applicable floor and target govern the product's single
price. A fact scoped to an unselected size does not affect the price or its
provenance.
For compatibility, an unhinted fact can still match when its full normalized
label appears in a bundle name. Individual bundle-name words are not used for
fallback matching, which prevents seller identity from activating an unrelated
fact.
## Where you set it
* **In the Library** — open your **Rate card** document to edit, preview, and
commit price rows. The Playbook's **Edit in Library** action opens this
document directly.
* **API integrations** — existing `save_playbook` and v2 settings writes still
accept pricing facts. Each write saves a new Library Rate card version.
* **REST** — `PUT /api/v2/storefront` with a `compositionPricing` object. The full
nested structure is returned on `GET /api/v2/storefront`.
* **On `/mcp/v3`** (preview) — `save_playbook` writes pricing, the
versioned selling guidance, and the brand/operator
[house discounts](/v2/storefront/house-discounts/overview) that come off
those prices, because the Playbook is the one noun that owns how you sell.
There, each fact's `pricing` object uses snake\_case names on both
`save_playbook` and `get`: AdCP's `pricing_model`, `currency` and
`floor_price`, plus `target_price` and `ceiling_price` in place of
`targetPrice` and `ceilingPrice`.
See [Seller pages](/v2/setup/seller-pages).
## How pricing reaches discovery
When a buyer discovers products, the Merchandising Agent reads your pricing facts,
matches them against the brief, and anchors each composed product's price
accordingly. The price surfaces on the discovered product as:
* `cpm` — the quoted price, in the pricing scope's currency.
* `currency` — ISO 4217, inherited from your `rateCard.currency` unless the buyer's
currency requires conversion.
* `pricingScope` — `public` (your open rate-card price) or `account` (a
buyer-specific price, e.g. after a [house discount](/v2/storefront/house-discounts/overview)).
Two guardrails always hold:
* **The wholesale floor wins.** Every quote is floored at the Seller Account's
wholesale cost. A pricing fact can raise or shape a price, but the agent never
composes a quote below cost.
* **Buyer currency must be accepted.** Discovery only returns priced products in a
currency on the Seller Account's accepted list; otherwise no products are returned
(see [cross-currency](/v2/concepts/cross-currency)).
## Playbook pricing vs. discounts
Playbook pricing sets your **base** pricing — what any buyer sees. To give a
*specific* buyer a better price, layer a discount on top:
Structured rate-card discounts keyed to a buyer's brand or operator.
Freeform per-buyer terms, notes, and operator-and-brand intersections.
## Related
Collect rate-card provenance alongside the rest of the publisher evidence pack.
How the agent turns what you have into a priced proposal.
The rules the agent follows when composing products.
# Multi-package products
Source: https://docs.apostra.com/v2/storefront/products/multi-package-products
A single storefront product that delivers through more than one package — distinct flight windows, each with its own budget, pacing, and delivery reporting.
Multi-package products are in beta. How the packages beneath a product are
surfaced to sellers may evolve before general availability.
A **package** is the delivery unit a buy actually runs on: it carries its own
budget, pacing strategy, and bid price, and reports its own delivery. Most
products deliver through a single package. A **multi-package product** is a
product delivering through more than one — a single product the buyer selected
once that fans out into several packages when the buy executes.
The common case is **flight windows**: a buyer splits their campaign into dated
runs — say a pre–Black Friday flight, the Black Friday–Cyber Monday peak, and a
holiday push. On execution, your product produces one package per window, each
with its own budget slice and pacing, all delivering against the same product.
You don't create packages, and neither does the buyer — the system creates them
automatically when the buy executes, one per flight window the buyer set on their
campaign. For the full package model, see the buyer's
[Package](/v2/buyer/campaigns/packages) reference.
## What it means for your product
* **Independent pacing per window.** Each package paces its own budget (`even`,
`asap`, or `front_loaded`) instead of averaging spend across the whole flight —
a holiday-peak window can spend fast while the lead-up paces evenly.
* **Per-window delivery reporting.** Each package reports its own `impressions`,
`spend`, and `clicks`, so delivery against your inventory is visible window by
window, not as one blended number.
## How it delivers
When the buy executes, one package is created per flight window, and each rolls up
under the buyer's media buy:
```
Media Buy
└── Package (Pre–Black Friday) budget · pacing · delivery
└── Package (Black Friday peak) budget · pacing · delivery
└── Package (Holiday push) budget · pacing · delivery
```
## Related
How packages sit under a media buy and report delivery.
Products assembled to a brief by the Merchandising Agent.
# Product composition
Source: https://docs.apostra.com/v2/storefront/products/product-composition
Products assembled to a buyer's brief on demand by the Merchandising Agent, instead of hand-authored one at a time.
Product composition is in beta. The behavior below is live for storefronts with
a ready Storefront-built product path; the surface may evolve before general availability.
**Product composition** lets your storefront assemble buyer-ready products on
demand instead of returning a fixed catalog to everyone. When a buyer sends a
brief, the [Merchandising Agent](/v2/concepts/storefront-agent) builds products
for it — selecting from your inventory building blocks, applying your
[operating instructions](/v2/storefront/operating-instructions/overview), pricing
them against your [Playbook pricing](/v2/storefront/playbook/overview), and
returning products shaped for the brief in front of it.
A composed product is indistinguishable from a hand-authored one to the buyer — it
carries a name, description, targeting, canonical `format_options[]`, and pricing. The
difference is that you never had to author it: the agent built it from your rules.
When a composed product is backed by your static avails feed, it also carries
an `ext.scope3_merchandise` block: the actual property each backing avail runs
on, its available delivery for that avail's own window, and any audience
facts your [product marketing](/v2/storefront/product-marketing/overview) has
taught for that channel — all resolved from your own data, never invented.
This block is not exclusive to composition: a `buying_mode: "wholesale"`
response carries the same `ext.scope3_merchandise` block for the same
static-avails-feed products, since it's read from your inventory data
directly rather than produced by composition. See
[the story-first proposal](/v2/storefront/proposal-story#the-plan-specifically-what-youre-buying)
for the full field-by-field explanation.
Composition is deterministic where it matters — the same brief against the same
inventory and rules returns products with stable ids, so a buyer who discovers
again sees the same products rather than a fresh set each time.
## How the product path is selected
Product composition is not a separately priced add-on and has no Storefront-wide
on/off switch. Each inventory source has an explicit product contract:
* **Storefront-built** supplies ingredients that Apostra turns into buyer-ready
products.
* **Agent-supplied** supplies complete products that Apostra preserves and routes
back to that Sales Agent.
* **Both product paths** supports both flows; each Source selects one or both.
The Sales Agent provider owns that declaration. Apostra Source pages show it
read-only. Apostra support handles declaration and both-path selection reconciliation;
Storefront operators cannot change the contract with a local toggle.
The Storefront-built path needs these things in place:
1. **A ready wholesale source.** The source must declare and implement the AdCP
3.1+ wholesale product contract. Ordinary buyer-facing products are never
guessed to be ingredients.
2. **Author operating instructions.** Composition can't run without rules — an
active [operating-instructions](/v2/storefront/operating-instructions/overview)
ruleset tells the agent what to build and how. If a Storefront-built path is
ready but no active ruleset exists, the storefront auto-pauses and notifies
you rather than composing blindly.
If diagnostics say the wholesale path is not ready, correct the Agent's product,
property, format, pricing, or execution declaration as reported, then ask Apostra
support to refresh or reconcile the capability. Apostra does not infer a new
mode from catalog contents.
Optionally, add or update your [Rate card document](/v2/storefront/library#add-a-rate-card)
so composed products are anchored to its floors, targets, and ceilings.
An [ad-server-backed source](/v2/storefront/inventory-sources/overview) has a
fixed Storefront-built contract. An ad server exposes raw
inventory — ad units and placements — which must be composed into buyer-ready
products before it can be sold; this is source behavior, not an account toggle.
## Provider adapter catalog snapshots
For a storefront connected directly to a supported provider, each successful
wholesale discovery refreshes a durable snapshot of every product the adapter
returned. The seller product list combines that snapshot with products from any
managed ad-server sources, using the adapter's stable catalog ids so products do
not appear new merely because a buyer started another discovery session.
The snapshot update runs after discovery and never holds up the buyer response.
That also means a newly connected adapter does not appear in the seller product
list until its first successful wholesale discovery finishes. The snapshot is a
catalog view, not permission to buy: every live buyer request still refreshes
account-specific availability and pricing and receives product ids scoped to
that buyer's provider account.
## What a buyer sees
Composed products are returned in ordinary product discovery. The buyer submits a
brief; the agent returns products (and, where configured, multi-product
**proposals** with budget allocations across them). Each product is priced
server-side and clamped to the `[wholesale floor, seller ceiling]` range, so a
composed price is never below your cost. When a product uses a signal you have
priced, the surcharge is added to that floor; see
[how signal pricing changes a quote](/v2/storefront/esa/signals#how-signal-pricing-changes-a-quote).
### Signals on a composed product
When the agent adds your signals to a product, it also chooses how each one is
used, in AdCP's signal targeting terms:
* A yes-or-no signal, such as an audience segment, is included or excluded.
* A signal with named values, such as a site-section key, is narrowed to the
values that fit the brief. When the signal lists its values, the agent picks
only from that list. An open key, such as a domain key, lists none, so your
ad server checks the chosen values when the media buy is placed.
* A numeric signal, such as an income band, is narrowed to a range inside the
range you declared.
The agent also decides how the signals combine. Signals that are alternatives
share a group, and the product reaches anyone who matches any one of them.
Separate groups are separate requirements: the audience must match every
group. For example, a product for decision makers at small and mid-sized
businesses puts "decision makers" in one group and the two company-size
segments in another. Excluded signals remove anyone who matches any one of
them. The agent adds a separate requirement only when the brief calls for it,
because each one narrows reach. It never puts one of your key-value signals in
the same group as an audience segment, because Google Ad Manager cannot treat
those as alternatives, and it keeps signals in different selection groups you
declare on a product as separate requirements. The product stores that
selection, and a media buy sends it to your ad server unchanged.
A product that narrows a signal to named values or a range, or that requires
signals together, needs every one of its signals to come from a managed sales
agent, so the agent does not offer such a product when any of its signals come
from another kind of source. Interchange creates every media buy with a managed
sales agent over AdCP 3.2, whatever AdCP version the buyer uses, so that
targeting reaches your ad server exactly as the product was built. A media buy
keeps the AdCP version it was created with for its later updates and reports.
A Google Ad Manager ad server connection accepts audience segments, keys whose
values are chosen per buy, fixed key-value pairs, and signals that combine
several conditions.
A composed product declares the AdCP `channels` of the inventory it combines:
every channel declared by the products and avails it was built from. A product
built from a display product and a CTV product declares both. A composed product
never guesses a channel from its creative formats, so it declares one only when
your source inventory does. Declare channels on your products and avails (see
[Make a product ready for proposals](/v2/storefront/inventory-sources/create-products#make-a-product-ready-for-proposals))
so buyers can find composed products by channel and see their delivery by
channel.
Creative delivery is canonical end to end. The composed product exposes the
selected URL-free `format_options[]`; when a buyer pins one with
`format_option_refs[]`, Apostra resolves it back to the exact source option
or direct `format_kind` and `params` selector. Source compatibility adapters may
normalize an older named format at ingestion, but composed products never expose
that source's legacy URL-based format identity.
That source option must still have the same identifier, format kind, parameters,
and publisher scope when the buyer submits the media buy. If the source product
changed after composition, the storefront rejects the stale selection and asks
the buyer to discover the product again. It never substitutes another option
merely because its dimensions look the same.
## Watching it work
Every composition run is observable. Use
[intelligence runs](/v2/storefront/intelligence-runs/overview) to see what the
agent composed for a given brief and why — the inventory it drew on, the rules it
applied, and the rationale behind each product.
## Related
The rules the agent follows when composing products — required for composition.
The pricing facts composed products are anchored to.
See what the agent composed for a brief, and why.
How the agent turns inventory + rules into a proposal.
# Publisher domains
Source: https://docs.apostra.com/v2/storefront/publisher-domains
How storefront publisher domains are stored and synchronized to a managed sales agent
## Overview
Use `businessProfile.publisherDomains` to tell Apostra which publisher
domains your storefront is authorized to sell. This is the canonical publisher
domain set for a managed storefront.
`operatorDomain` is separate: it identifies the company or domain operating the
storefront. For network sellers, the brand domain is often not the same as
the publisher domains being sold. A third domain — the one your account is
registered under — decides whether your brand domain can be verified at all.
See [Which domain is which](/v2/concepts/domains) if any two of them differ.
The top-level `publisherDomain` field on `POST /storefront` and
`PUT /storefront` is legacy metadata. Keep using `operatorDomain` for the
storefront identity and `businessProfile.publisherDomains` for the publisher
domains the storefront can sell.
## Discovery publication origins
A Discovery publication origin is separate from the publisher-domain set
above. It is the verified hostname where an approved, public listing
(previously called the media kit or Discovery Card) is served. A registered origin serves the last verified published revision while
its hostname verification remains active. When a replacement revision is
published, the verified revision stays live until the replacement passes its
checks. Drafts, private publications, suspended or expired registrations, and
revisions that have never been verified fail closed.
### Publish from the Listing page
Seller admins manage this from the storefront's **Listing** page (the retired
Settings → Discovery & distribution tab, and the retired Branding &
distribution page, both redirect here). The page previews the canonical
listing identity and reads the name, description, website, logo, and colors
from the storefront's `brand.json` projection. Directory and composer icons
are generated from that logo when the card is published, so those values do
not need to be uploaded or synchronized separately.
The Listing page has four steps, completed in order: **Identity**, **What you
do**, **Listed on Apostra marketplace** (a switch included with every
account — turning it off unlists the storefront, with a confirmation), and
**Public distribution**. There is no segmented Apostra/Public control and
no selectable "Hidden" option — a hidden storefront can never be found, so it
isn't a distribution choice. A storefront that hasn't published yet is shown
as a state, **Not listed yet**, never as a step to pick.
An active, unexpired [Demo Storefront](/v2/storefront/inventory-sources/demo-storefront)
counts only **Identity** and **What you do** towards Demo readiness. Marketplace
publication and Public distribution remain optional choices. The Demo does not
publish itself or add a paid Distribution package to make its setup complete.
Expired and inactive Demos return to the ordinary four-step model.
**Public distribution** is the gate for every app destination, and it has two
parts: add the Distribution package (the paid upgrade), then point your
domain at Apostra with one CNAME at your registrar — Apostra checks
it for you. Once the domain verifies, choose **Make it public** to take Public
distribution live — verifying alone does not publish — and verifying also
requires the storefront to already have a published Apostra listing
(step three, above). Apostra first-party storefront is attached to
the platform-managed `api.apostra.com` origin automatically; it does not
need a CNAME or a separate storefront hostname, and never shows the domain
step.
An already-listed storefront returns to **Not listed yet** by turning off
the **Listed on Apostra marketplace** switch (step three), which asks for
a confirmation. Unlisting takes the storefront off Apostra and the public
web; buyers and directories can no longer find it until it's published again.
An app still served from its own legacy per-app hostname is not affected: it
keeps serving until the hostname is disabled or the app moves to your public
listing domain.
Below the four steps, **Distribution** shows the destinations where buyers
find the listing, as tabs: the **Listing card** (a downloadable PDF) comes
first (its tab is always shown; the download becomes available once your
listing is live on Apostra), then app destinations once Public
distribution is live — the **ChatGPT app** today, with Claude, Slack,
Discord, and WhatsApp listed in the add-destination picker as coming soon.
Before Public distribution is live, the app-destination tab is dimmed with a
note that apps unlock with that step.
OpenAI-specific submission values are entered in the OpenAI portal and are
not copied into the listing; the directory category is the exception —
Apostra sets it to Business for new registrations (an existing
registration keeps its saved category), declares it in the package, and
shows it before you download. OpenAI's
domain-ownership proof is one of the ChatGPT destination's own steps — paste
the token OpenAI gives you and publish it at the address they check — not a
control on the domain step.
The public routes depend on how the origin is hosted:
| Hosting | Card | JSON | Embed |
| - | - | - | - |
| Dedicated customer hostname | `https://{hostname}/` | `https://{hostname}/card.json` | `https://{hostname}/embed` |
| Apostra first-party publication | `https://api.apostra.com/discovery` | `https://api.apostra.com/discovery/card.json` | `https://api.apostra.com/discovery/embed` |
The first-party card uses `/discovery` because `api.apostra.com` is also the
shared API origin. It does not replace the API root. Dedicated customer
hostnames can serve their card at `/`.
Every registered origin also exposes a routing proof at
`/.well-known/interchange-discovery-registration`. Apostra periodically
rechecks the hostname, HTTPS response, publication identity, and replacement
revision. Public serving switches revisions only after those checks pass, and
stops when active verification expires or the publication is no longer public.
### OpenAI Apps domain verification
The ChatGPT destination's domain-ownership step stores the Apps challenge
token OpenAI provides on the active hostname registration. The exact token is
then available as plain text, with caching disabled, at:
```text theme={null}
https://{hostname}/.well-known/openai-apps-challenge
```
For Apostra first-party publication, that URL is
`https://api.apostra.com/.well-known/openai-apps-challenge`. The challenge
URL is fixed by OpenAI and is not nested under `/discovery`.
Replacing or removing a token requires explicit confirmation. A replacement
invalidates the previous value immediately, and publication, replacement, and
removal events are retained in an immutable audit history. The protected
management view displays the saved token, its public URL, and whether the latest
reachability test returned the exact value. That test does not call OpenAI or
claim that OpenAI has verified the domain.
Publishing remains explicit. Saving storefront branding does not change
Discovery visibility or advance its immutable public revision.
## Set publisher domains
Send the full current domain set on the storefront business profile:
Only account admins can add, replace, or remove publisher domains. Other
members can view and refresh the property roster, but its domain-management
controls are read-only.
The REST `PUT` below replaces the supplied business profile, so send its full
current value. MCP clients can instead call `update_business_profile` with only
`publisherDomains`; fields omitted from the profile state read for that call
are preserved. Overlapping patch calls use last-writer-wins behavior.
```bash curl theme={null}
curl -X PUT https://api.apostra.com/api/v2/storefront \
-H "Authorization: Bearer scope3_..." \
-H "Content-Type: application/json" \
-d '{
"businessProfile": {
"publisherDomains": ["mediavine.com", "cafemedia.com"]
}
}'
```
Apostra normalizes valid domains to lowercase, deduplicates them, and saves
the sorted set. Invalid placeholder values are ignored.
## Managed sales-agent sync
For a storefront backed by an Apostra-managed sales agent,
`businessProfile.publisherDomains` is also synchronized to that managed sales
agent as the authoritative publisher-domain set.
When the set changes:
* Added domains are registered with the managed sales agent.
* Removed domains are cleared from the managed sales agent's authorization
projection.
* Current domains are rechecked against AAO and `adagents.json` so their
authorization status stays fresh.
## Authorization status in setup
Storefront setup lists each configured publisher domain separately. A domain can
be:
| Status | Meaning |
| - | - |
| Checking | The first AAO and `adagents.json` resolution has not completed. |
| Authorized | The resolved publisher document positively authorizes the applicable storefront agent. |
| Not authorized | The publisher document was resolved but does not authorize the applicable storefront agent for the intended scope. |
| Authorization unknown | No publisher document, an unreadable one, or no conclusive authorization evidence. |
| Check failed | The latest publisher-document resolution failed. |
The underlying `adagentsStatus` separates "there is no file" from "there is a
file and we cannot read it", because the fix is different:
| `adagentsStatus` | Meaning |
| - | - |
| `resolved` | A usable `adagents.json` was retrieved. |
| `no_adagents` | We looked and found no file. Publish one. |
| `invalid` | A document is published at the well-known path and is not a usable `adagents.json` — commonly an HTML page or a redirect served instead of JSON. Fix the file; do not publish a second one. |
| `error` | The resolution could not complete (timeout, DNS, 5xx). Retried automatically. |
| `pending` | Not yet resolved. |
Declaring a domain (`POST /api/v2/storefront/publishers`, or a business-profile
update) kicks off its `adagents.json` resolution immediately — you do not wait
for a background sweep. The result usually lands within seconds; re-read the
publisher list to see it. A failed immediate resolve is retried by the sweep.
Re-declaring a domain you already configured re-checks it the same way,
subject to a short cooldown (currently 10 minutes) since its last completed
sync — so if you just fixed your `adagents.json`, re-declaring the domain (or
resending it in a `PUT` of the full set) gets you a fresh answer immediately
instead of waiting for the sweep. Re-declaring inside the cooldown is a no-op:
it does not reset the domain to "Checking" or trigger another fetch.
Changing the effective inventory-source authorization identity also resets the
current result to **Checking**. This happens when the storefront moves between
a usable external-only identity and operator-run mode, or when an external-only
storefront changes its active agents or one of their URLs. A storefront with no
usable external agent URL already uses `https://interchange.io`, so adding or
removing its first operator-run source does not reset the result. Adding another
operator-run source also keeps the same identity.
Apostra rechecks each configured publisher automatically.
## Why a domain is not authorized
Every resolved domain also carries a machine-readable `authorizationReason`
naming the exact outcome, so you know what to fix rather than seeing a bare
"not authorized":
| `authorizationReason` | What it means | What to do |
| - | - | - |
| `no_file` | No `adagents.json` was found (a definite 404 at `https://{domain}/.well-known/adagents.json`, and no ads.txt `MANAGERDOMAIN` delegation resolved one). A transient fetch failure does not set this reason. | Publish an `adagents.json` on the publisher domain. |
| `file_invalid` | A file was retrieved but cannot be used: unparseable JSON, wrong shape, or an invalid typed authorization entry. Repeating the same canonical agent URL is supported when separate entries grant separate scopes; those scopes are unioned. | Fix the file so it parses as a JSON object with `authorized_agents[]`, and give every entry a valid typed selector. |
| `agent_not_listed` | The file is valid but your sales agent's URL is not in `authorized_agents[]`. | Add the agent URL to `authorized_agents[]` with a typed authorization. |
| `agent_untyped` | The agent is listed, but its entry carries no authorization mapping the platform can resolve — most often a pre-3.x file with bare `{url, authorized_for}` entries; also an unrecognized `authorization_type`, or a known type whose selector is missing or empty. Authorization is a function of the per-entry `authorization_type` + selector, so these entries fail closed. | Give the entry a resolvable typed authorization: `authorization_type` (`property_ids`, `property_tags`, `inline_properties`, or `publisher_properties`) plus its matching, non-empty selector. |
| `agent_not_property_bound` | The agent's typed entries are well-formed but grant no property authority on this publisher domain — the selector matched nothing, the resolved properties belong to other domains, or the entry is signals-only (`signal_ids`/`signal_tags`), which never conveys property authority. If the file resolved via an ads.txt `MANAGERDOMAIN` delegation, the `adagents.json` that needs the fix belongs to the manager domain, not yours — coordinate with the manager to map your properties there. | Map the agent to properties on this domain: fix the selector, add the domain's properties to the file, or add a property-typed entry beside a signals-only one. |
| `authorized` | The resolved scope authorizes the agent for this domain. | Nothing — you're live. |
The reason appears on `GET /api/v2/storefront/publishers` items and on each
domain in the readiness checklist, which also summarizes the most common fix
(for example, "2 domains need an adagents.json upgrade to typed
authorizations").
Publisher-domain declaration is a go-live readiness check. Setup remains
incomplete until at least one publisher domain is configured. Product mapping
to the configured set is shown as a rollout warning while older catalogs are
backfilled. Authorization is shown separately as advisory provenance and does
not block transactions.
The status also shows the number of resolved properties when available. That
count describes what the current publisher document contains; it does not prove
that a network's document includes every site the network represents. Network
sellers should compare it with their own managed-site roster before enabling
buyer-visible coverage — the roster's `propertyCount` is the number to compare
against, and it agrees with this one.
If the managed sales agent cannot accept the replacement set, Apostra keeps
the prior local publisher-domain sync state instead of deleting rows first. That
lets the update retry without losing the previous authorization projection.
`businessProfile.publisherDomains` is the whole set, so send all of it each
time. To remove `cafemedia.com` and keep `mediavine.com`, send
`"publisherDomains": ["mediavine.com"]`. If you only want to add or remove one
domain, use the single-domain routes below instead — they do not require you to
restate the rest, and restating a set you read incompletely is how domains get
deleted by a request that meant to add one.
## Adding or removing one domain
Two routes change exactly one domain and leave every other one alone:
| Route | Behavior |
| - | - |
| `POST /api/v2/storefront/publishers` | Declare one domain. Body `{ "domain": "vox.com" }`; full URLs are accepted and normalized. Re-declaring a domain you already have is not an error — it re-checks it, subject to the cooldown above. |
| `DELETE /api/v2/storefront/publishers/{domain}` | Un-declare one domain. Returns `204`, including when the domain was already absent, so retrying is safe. |
Prefer these over `PUT /api/v2/storefront/publishers` (or a business-profile
update) whenever you are changing one domain. The `PUT` replaces the entire
declared set: any domain you declared that is missing from the body is deleted,
and it has no conditional-write check, so a set you assembled from an earlier
read can also discard a change someone else made in between. The single-domain
routes have neither problem, because they never send a set.
The `PUT` applies as a diff, so it is safe to resend a set you already have. A
domain that is in both your current set and the body keeps its row and
everything resolved under it — its properties, AdCP Collections, creative formats,
your own property claims, and its authorization verdict. Only the declared
domains you left out are deleted, and only domains that were not there before
are created (those start at `pending`, since nothing has resolved them yet).
Only domains you declared are yours to remove, whichever route you use. `DELETE`
returns `409` when the domain exists but its provenance is not `declared` — a
domain we discovered or crawled for you follows its own source, and nothing is
changed. The `PUT` behaves consistently rather than silently: it replaces your
declarations only, so a discovered domain left out of the body stays on the list.
Leaving one out is not a way to delete it.
## Empty or missing values
An empty array means "clear the publisher-domain set" for the storefront:
```json theme={null}
{
"businessProfile": {
"publisherDomains": []
}
}
```
Omitting `publisherDomains` means the authorized domains are unknown or have not
been captured yet. It does not authorize the storefront to sell every domain.
Setting `businessProfile` to `null` clears the business profile and clears the
publisher-domain set for managed sales-agent sync.
## Buyer visibility
Updating the publisher-domain set does not by itself change the storefront name,
operator identity, or buyer-facing seller profile. Buyers may see publisher
domain coverage on products when those products expose
`publisher_properties`; see [Publisher properties and coverage](/v2/storefront/inventory-sources/publisher-properties-coverage).
Authorization is necessary but does not by itself enable buyer disclosure. The
storefront's resolved property coverage must also be synchronized and its
coverage-disclosure rollout enabled.
## The property roster
Every declared publisher domain is resolved through two deliberately separate
reads. AAO's publisher lookup supplies creative format declarations, including
AAO registry declarations for platforms that do not publish their own file.
If that lookup is temporarily unavailable and the successfully resolved live
publisher file explicitly declares `formats[]`, those authoritative
publisher-origin declarations keep the format catalog current until AAO is
available again. A successful AAO lookup still owns publisher/community
precedence.
The live publisher-origin `adagents.json` read supplies authorization and
continues to fail closed. The **property roster** is the canonical seller view
of the result: each domain's declared `properties[]` (with their identifiers,
types, and tags), its `collections[]` (canonical AdCP Collection identity
`{publisher_domain, collection_id}`), its canonical creative `formats[]`, and
the authorization outcome.
Fetch it with `GET /api/v2/storefront/property-roster`, the
`get_property_roster` operation, or by asking your agent to open the Property
Roster page (the `open_property_roster` tool in any MCP host).
The response includes `permissions.canManagePublisherDomains`. When it is
`false`, the caller may still read and refresh roster data but must ask an
account admin to change publisher domains or declared properties.
Each publisher in the roster carries:
* `authorizationStatus` — `authorized`, `unauthorized`, or `unknown`.
* `authorizationReason` — the distinct resolution outcome behind that status:
`no_file` (no adagents.json found), `file_invalid` (the file could not be
read), `agent_not_listed` (the file does not list your sales agent),
`agent_untyped` (your agent is listed in the pre-3.x format without a typed
authorization mapping), `agent_not_property_bound` (typed entries exist but
none map your agent to this publisher's properties), or `authorized`.
`null` means the domain has not yet been resolved under the reason contract.
* `properties[]` and `collections[]` — Properties and AdCP Collections
persisted from the most recent
successful resolution. Entries a newer document no longer declares are
removed from the live roster (history is retained internally). A publisher
that stops serving an adagents.json entirely has its declared identity
removed from the live roster until the file returns.
* `formats[]` — canonical, URL-free `formatKind` + `params` declarations from
the AAO publisher lookup, or from an explicit live publisher declaration
while that lookup is unavailable. Each entry retains its publisher
property-id/tag scope, the live roster properties that scope matches, and
provenance: `publisher_adagents` for publisher-origin declarations or
`aao_registry` for an AAO registry declaration. If neither lookup provides a
definitive catalog, the roster preserves the last successful catalog instead
of treating the failure as an empty catalog.
* `formatCatalogSyncedAt` — when the format catalog was last definitively
reconciled from AAO or an authoritative live-origin fallback. This timestamp
is independent of `lastSyncedAt`, which describes the live-origin
authorization and identity check. Use the catalog timestamp to judge the
freshness of preserved formats.
* `propertyCount`, `collectionCount`, and `formatCount` — how many the domain
actually has. For a large network these can be bigger than the corresponding
arrays are long, because a single roster read is size-bounded. Read the
counts, not the array lengths, when you want to know what a domain covers.
The response also carries `totals` for the whole storefront and a `truncated`
object:
```json theme={null}
{
"totals": { "publishers": 1, "authorizedPublishers": 1, "properties": 6843, "collections": 0, "formats": 4 },
"truncated": { "publishers": false, "properties": true, "collections": false, "formats": false }
}
```
`totals` always describes the storefront, never the response. When any
`truncated` flag is `true`, the response left rows out — so **an absence in
`properties[]` is not evidence a property is missing.** Look the specific
property up, or open the Property Roster page, rather than concluding a site
is not covered.
### Whether one property is authorized
Every property carries its own `authorization`:
```json theme={null}
{ "status": "unauthorized", "grain": "domain", "reason": "agent_not_listed" }
```
Read `grain` before acting on `status`. Authorization is granted by the
publisher domain's `adagents.json`, and that is where we resolve it, so:
* **`unauthorized` is definite.** The domain does not authorize your agent, so
nothing under it can be sold — however many properties the file declares.
* **`authorized` is inherited from the domain** (`grain: "domain"`), not proven
against the specific selector mapped to your agent. Under AdCP 3.x a file
can map an agent to a subset of the properties it declares.
* **`unknown`** means no publisher-origin declaration backs this property —
most often a property you declared yourself, which stays on your roster but
cannot read as authorized until the publisher's file declares it.
### Storefronts where coverage does not apply
The response also carries `coverageApplies`. It is `false` for a storefront that
sells through a platform account connection (Meta, Google, TikTok and the like):
selling rights come from that connection, no `adagents.json` will ever resolve,
and the authorization verdict is not something to fix. Do not read those
verdicts as a problem, and do not publish a file to change them.
The header totals include the number of discovered formats, and each publisher
card can expand its format list. An unscoped declaration applies to every
property under that publisher; a scoped declaration matches a property when
either its `propertyId` is listed or one of its tags overlaps.
Discovered publisher formats describe capacity — what the publisher says its
inventory accepts. A product's `format_options[]` is the closed executable set
that a buyer can actually purchase from that sales agent, so it may be
narrower. AAO registry provenance never authorizes a sales agent.
In Property Roster, **Create product** opens **Create a product**
with that publisher format already selected. Choose an **Ad server**,
select the exact inventory to package, and choose one or more **Delivery
formats** reported by that source. Select **Review product**, then **Create
draft product** after validation succeeds. The format stays in the task instead
of being sent back through chat.
**Review property mapping** means the complete publisher roster was checked and
the format did not match a current property. **Mapping not shown** and **Check
inventory mapping** mean the roster view was size-bounded, so it does not
contain enough properties to prove whether the format matches. Use the check
action to inspect the mapping instead of treating the format as unavailable.
Format IDs, sources, and the complete creative parameter object remain under
**Technical details**.
When a product uses a publisher-catalog option, copy its `format_option_id`,
`publisher_domain`, `format_kind`, and `params` into the product's
`format_options[]`. Product validation compares that identity and canonical
kind with the connected roster and warns when the option is unknown, uses a
different kind, or applies to none of the product's selected publisher
properties. The product may narrow the catalog option's parameters. These are
authoring advisories: a catalog read failure does not block a product write or
change the sales agent's own validation verdict. Product validation does not
yet check placement-specific format availability.
The roster only reflects what publishers declare — Apostra never invents
property identity. To appear authorized, the publisher's `adagents.json` must
list your sales agent URL with a typed authorization mapping
(`authorization_type` plus the matching selector, e.g. `property_ids`);
listing the URL alone is not authorization under AdCP 3.x. See
[adagents.json](/v2/storefront/inventory-sources/adagents-json) for the file
format and deployment guidance.
### Declaring properties before the publisher does
A seller can record properties conversationally — through their agent or via
`POST /api/v2/storefront/property-roster/properties` (the
`declare_roster_property` operation) — before the publisher's adagents.json
declares them. Declared entries carry `provenance: seller_declared` and are
visibly distinct from publisher-origin identity:
* A declared property needs at least a `propertyId`, an identifier, or a
name. The roster never invents identity.
* When the publisher's adagents.json later declares the same property, the
publisher-origin record supersedes the declared one (declaring it again
returns `outcome: already_resolved`). Your claim is retained either way: if
the publisher later stops declaring the property, it returns to the roster
as your declaration instead of disappearing.
* adagents.json changes never remove seller-declared entries; remove one with
`DELETE /api/v2/storefront/property-roster/properties/{domain}/{propertyKey}`
(`remove_declared_roster_property`). Publisher-origin entries cannot be
removed this way — they follow the publisher's file.
## Publisher onboarding
Request property-roster evidence together with avails, pricing, lifecycle
ownership, and proof briefs.
# Management UI
Source: https://docs.apostra.com/v2/ui-guide
Navigate Apostra platform dashboard to manage members, API keys, advertisers, partners, and billing
Apostra platform provides a web dashboard at [app.apostra.com](https://app.apostra.com) for managing your organization's members, API keys, advertisers, partners, and billing.
## Apostra assistant
The product assistant is moving from **Murph** to **Apostra**. During the staged
rollout, your account may still show Murph until Apostra branding is enabled for
you. Once enabled, the assistant and its calls to action use Apostra, including
**Ask Apostra**.
New links use `/{customerId}/apostra`, including
`/{customerId}/apostra/c/{conversationUid}` for a conversation. Existing
`/murph` bookmarks and shared conversation links continue to work: Apostra
redirects them to the matching new route without dropping supported page
filters or the conversation ID. You do not need to update existing links.
API and agent integrations keep their current compatibility identifiers during
this rollout. Continue to use `/api/v2/murph`, `/mcp/v2/murph`, and the
`ask_murph` tool. A future identifier change will use an additive alias and a
published deprecation period.
### Dictate a message
Select the microphone in the message box, speak, then select it again to stop.
The assistant turns your speech into text and places it in the message box at
your cursor, so you can check and edit it first. Nothing goes to the assistant
until you send the message.
* A recording stops on its own after two minutes and is transcribed.
* If the recording has no speech in it, such as silence or background noise,
nothing is added to your message and you see a "no speech" notice. Speak at
a normal level close to your microphone.
* The first time, your browser asks for permission to use the microphone. If
you block it, allow microphone access for the Apostra site in your browser
settings to use dictation.
* The microphone appears only in browsers that can record audio.
* Dictation is unavailable once you reach your daily usage limit, until it resets.
**What happens to your recording.** The recording is sent to Apostra and
transcribed by Google Cloud Speech-to-Text in the United States. Apostra does not
store the recording. Only the text you choose to send becomes part of the
conversation. To spell names correctly, the transcription request includes the
names of your account's own advertisers, campaigns and storefront, along with
common advertising terms, as spelling hints. Names from one account are never
used for another account. Transcription uses the language you chat with the
assistant in.
## Account Switcher
If you belong to multiple accounts (for example, a Buyer account and a Seller account), use the account selector in the top-right corner to move between them. It also appears when you have a pending account invitation. A standalone account with no invitation or other account to reach does not show a redundant selector.
For organizations with the new settings group, the selector lists switchable accounts and pending invitations. Organization administrators add, archive, and restore child accounts in **Account settings → Organization → Accounts**. Organization members and billing details are in the adjacent panes. Until that group is available, the existing organization controls remain in the selector.
***
## Buyer Homepage
After logging in as a **Buyer**, you'll see the buyer homepage with quick-access cards for campaigns, advertisers, API keys, and member management.
From here you can:
* **Get started** with documentation and quickstart guides
* **View reporting** for campaigns and media buys
* **Manage creatives** for your campaigns
* **Manage advertisers** for your organization
* **Manage API keys** for your integrations
* **Manage members** and permissions
***
## Seller Homepage
After logging in as a **Seller**, you'll see the seller homepage with quick-access cards for your storefront, campaigns, API keys, and member management.
From here you can:
* **Get started** with documentation and quickstart guides
* **View your storefront** and manage products and inventory
* **View reporting** for campaigns and media buys
* **Manage API keys** for your integrations
* **Manage members** and permissions
***
## API Keys
Click **Manage API keys** from the dashboard to view your existing keys.
The primary list contains your keys. For organization administrators, it contains
user-owned keys across the organization so an admin can inspect their access levels
and revoke them. Each key acts as its owner: it follows that person's account and
resource access, while its selected `interchange:read`, `interchange:write`, or
`interchange:admin` access level can narrow what the integration may do. Account
administrators also see existing account-owned and legacy keys in separate migration
sections.
### Creating an API Key
Click **+ Create API Key** to open the creation form.
Fill in the following fields:
| Field | Required | Description |
| - | - | - |
| **Name** | Yes | A descriptive name for the key |
| **Description** | No | Additional context about the key's purpose |
| **Expiration Date** | No | Optional expiry date (max 1 year from today). Leave empty for no expiration |
The API key secret will be shown after creation. Store it securely - you'll need it to authenticate API and MCP requests.
For durable shared automation, create a dedicated service user, assign that user only
the accounts and resources the integration needs, and create the key as that user.
***
## Advertisers
Click **Manage advertisers** from the dashboard to view and manage advertiser accounts.
The advertiser list shows each advertiser's name, brand domain, description, status, and creation date. Use the three-dot menu on each row to manage individual advertisers.
### Creating an Advertiser
Click **+ Create advertiser** to open the creation form.
| Field | Required | Description |
| - | - | - |
| **Name** | Yes | The advertiser's name |
| **Brand domain** | Yes | The advertiser's brand domain. Use **Look up brand** to resolve and link the brand from the domain. |
| **Primary currency** | Yes | The currency every campaign under this advertiser spends in. You can change it until the first campaign is created, after which it is locked. |
| **Description** | No | Additional context about the advertiser |
| **Sandbox mode** | No | Routes the advertiser's operations to sandbox accounts — no real platform calls or spend. Cannot be changed after creation. |
#### Adding an advertiser in chat
You can also add an advertiser without leaving the chat: ask the assistant (for example, *"add an advertiser called Acme at acme.com"*) or click **+ Add advertiser** on the Advertisers overview. A guided in-chat form opens — pre-filled with anything you already said — capturing the same fields above, resolving the brand for confirmation, and creating the advertiser when you submit. Once it's created you can switch straight into it or add another.
#### Media Company advertiser setup
In a Media Company account, select **Add advertiser** from Advertisers or
Campaigns to open the same guided form. Sandbox is available to every Media
Company and makes no real platform calls or spend. Accounts enrolled for live
campaign management can explicitly choose **Live**; other accounts see Sandbox
only. After creation, the roster refreshes before you switch into the new
advertiser.
***
## Member Management
Click **Manage members** from the dashboard to view and manage your organization's members.
The member list shows each user's name or email, role, number of assigned advertisers and partners, and their status. Pending invitations show as "Invitation sent" in the Date Created column. You can search for members and change their role using the dropdown in the Role column.
### Inviting Members
Click **Invite members** to add new users to your organization.
**Step 1: Enter email and role**
* Enter one or more email addresses
* Select an **Access role**:
* **Admin** - Full access to all Advertisers and Partners by default. Advertiser selection in the following step is optional.
* **Member** - Limited access. You must assign specific Advertisers and Partners in the next step.
**Step 2: Select access**
Select which **Advertisers** and **Partners** the new member should have access to. For Admin users this step is optional since they have access to everything by default. Click **Send** to send the invitation.
***
## Next Steps
Learn how to use your API key to authenticate requests
Get started making API calls with your key
# Manage ads
Source: https://docs.apostra.com/v3/manage-ads
Plan campaigns, manage advertising, and read results in Apostra or through a connected assistant.
Use Apostra to plan and manage advertising, sell inventory, or understand
results. You can work in the [application](https://app.apostra.com) or connect
an assistant to the V3 MCP endpoint. You do not need to build software first.
Add the hosted MCP connector, sign in, and verify your account with a read.
Set up your account, prepare campaigns, and review what is needed to launch.
Set up a Seller Account, connect inventory, and manage incoming buying work.
Understand delivery and reporting for your account.
Account permissions and workflow readiness still determine what you can do.
Connecting an assistant does not grant additional access or approve spending.
Some shared guides retain `/v2/` in their URL; follow their stated API and
workflow requirements.
If you want to build a marketing assistant, automate a report, connect business
data, or write an advertising agent, follow [Build software](/v3/overview).
The developer quickstart installs `apostra-developer` in Codex or Claude Code.
# Get support
Source: https://docs.apostra.com/v3/support
How to get help with Apostra, from the in-product assistant to the support team.
## Ask in the product
Signed in to [app.apostra.com](https://app.apostra.com), ask the assistant
(**Ask Apostra**, or **Murph** while your account is still on the older
branding). See the
[Management UI guide](/v2/ui-guide#apostra-assistant).
## Contact the support team
Email [support@apostra.com](mailto:support@apostra.com).
To get an answer faster, include:
* your account name or ID
* the IDs of the objects involved, such as the advertiser, campaign, media buy,
or creative
* what you did, what you expected, and what happened instead, with the time it
happened
* any error code and message you received, for example from a
[v3 error](/v3/errors)
## Check the docs first
* [Errors](/v3/errors) explains each error code and how to recover.
* [Rate limits](/v3/rate-limits) covers request limits and retries.
* The [changelog](/v3/changelog) lists recent changes that may affect you.
# Changelog
Source: https://docs.apostra.com/v2/changelog
Release notes and changes to API v2
# API v2 Changelog
All notable changes to the Interchange API v2 are documented here.
## 5.962.0 — October 8, 2026 at 9:36 AM UTC
* Fewer proposals lose their written argument to the fact check. Before it trusts a batch of sentences, the fact check tests itself with two sentences it knows are true, one taken from your Library or catalogue and one from your Playbook. Those test sentences were sometimes malformed: a Playbook excerpt cut off mid-sentence, or a fact with an extra full stop added. A correct check could reject them and throw the whole argument away. They are now complete sentences taken word for word from your sources. The check is otherwise unchanged. Nothing to change on your side.
* Murph makes fewer failed tool calls on accounts that run on Gemini. When Murph answers with Gemini, its tool calls now have to match each tool's published input schema before they are sent, so a call no longer fails because it carried a field the tool does not accept. Nothing changes in how you use Murph, and the V3 tools, their inputs and their validation are unchanged for every MCP client.
* Murph in a seller account no longer offers notification settings to accounts that don't have them yet. Before, Murph could try to read your notification settings and get "notification\_config is not enabled for this account yet." Now Murph's seller tools follow the same access check as the V3 MCP surface, so Murph only offers notification settings when your account has them.
* When a V3 tool call includes a key the tool does not accept, the error now lists every key that object accepts. Long lists are no longer cut off after 50 keys. When the key belongs in a nested object, the error also says where it goes: for example, a `search` filter field sent at the top level gets "filter fields go inside filter". The error still never repeats the key that was rejected.
## 5.961.0 — October 8, 2026 at 9:09 AM UTC
* A reviewer access request that is still setting up can now be cancelled from the ChatGPT destination with **Cancel request**, so a request that never finished no longer blocks you from requesting again.
* Products now list the targeting they declare in `supported_targeting`, including targeting declared through AdCP 3.2 `overlay_support`.
* When a Meta media-buy readback is available, `get(kind: "media_buy")` now shows when the provider was checked, whether the result is current, and any open configuration drift or newly discovered creatives. No action is required.
## 5.960.0 — October 8, 2026 at 8:50 AM UTC
* Click-URL parameters (`utmConfig` in the v2 API, `macroAdditions` in V3) now accept every AdCP universal macro, including `{DOMAIN}`, `{APP_BUNDLE}` and `{PAGE_URL}`, so you can save a value such as `utm_content={CREATIVE_ID}_{DOMAIN}{APP_BUNDLE}`. Apostra fills only `{CREATIVE_ID}`, `{CAMPAIGN_ID}`, `{MEDIA_BUY_ID}` and `{PACKAGE_ID}` when a creative syncs to a seller, using its own ids. Every other macro is known only when the ad serves, so it has to stay in the click URL for the seller or Apostra's click redirect to fill, and we are turning that on account by account. Until it is on for your account, a parameter that carries one of those other macros is left off the click URL on every sync, including campaign-level creative syncs, which used to send it with the macro as literal text. Parameters made only of literal text or Apostra's own ids are unchanged, and you don't need to change anything you have already saved. See "Click-URL parameters" in the advertiser guide.
* Campaigns with an age range now launch on Meta storefronts that approve media buys automatically. The age range is sent to Meta, which applies it, instead of the media buy being rejected. On other storefronts a campaign age range can still stop the media buy at launch, so remove it before launching there.
* Buyers are no longer shown a saved composed product that narrows a signal to specific values, or requires signals together, unless every one of its signals comes from a managed sales agent, the only source that can deliver that targeting. Refining such a product, or finalizing a proposal that includes it, now returns `unable` with the reason, instead of the media buy being refused later.
* If an advertiser locked your Buyer Account's operator identity before you chose its account scope, you can now confirm it as one operating unit as well as the whole operator, with `save_buyer_operator` and the same operator domain. This lets a company that runs several buyer accounts under one domain, such as one per region, give each its own unit. A unit is refused while a seller account already holds your identity without a unit, and the error says which kind; support can move those accounts.
* Media buys for composed products that narrow a signal to specific values, or require signals together, now go through for every buyer, including buyers on AdCP 3.1 and media buys Interchange places for campaigns. Before, they were refused with an error asking for AdCP 3.2. Interchange now creates every media buy with a managed sales agent over AdCP 3.2, whatever version the buyer uses, so the targeting reaches the seller's ad server as the product was built. A media buy keeps the AdCP version it was created with for its later updates and delivery reports, so buys already running are unchanged.
* Storefronts on the Storefront-built and Agent-supplied paths no longer refuse a product search because its brief states an age requirement, such as "please target women 18-55" or "exclude anyone under 25". The brief now reaches the seller's inventory, so the seller can recommend age-specific inventory or an age signal it sells. Age stated only in the brief is not applied as targeting: each product returned for such a brief says so in its description and explains how to apply it, by sending `targeting_overlay.demographics.age` on an AdCP 3.2 request. A structured age range is still applied exactly, and briefs that only describe the audience, state a preference, or contain budgets and durations come back with no note.
## 5.959.0 — October 8, 2026 at 8:08 AM UTC
* A rate card you upload now waits as a Draft; Save and update prices shows which prices change, and flags any below a floor, before anything goes live.
* Storefronts whose sales agent requires operator authentication now handle buyer accounts like ad-server adapters. Before, Apostra forwarded a new buyer's account request to that sales agent, which doesn't accept account requests that way. Now the storefront reviews the request under its own account approval settings, and the seller links the account to the sales agent's own account in Buyer Account Mapping. Storefronts whose sales agent accepts account requests still forward them, as before.
## 5.956.2 — October 8, 2026 at 6:03 AM UTC
* The V3 campaign launch preview no longer warns that a creative "needs seller approval before launch" for Amazon, Meta, Reddit, Snap, and Spotify media buys when the creative simply hasn't synced to the platform yet — syncing happens automatically after launch for these sellers, so the warning was never accurate. A genuinely rejected, in-review, or missing creative still blocks as before, for every seller.
## 5.956.0 — October 8, 2026 at 5:37 AM UTC
* Seller administrators can now confirm an owned Sales Agent's product mode or correct the mode for their own binding to an unclaimed Agent through V3. This keeps existing connected inventory on the intended product path without a support request.
* Buyers have no new setup step or documentation to read. A correction can change the product path they receive for that one seller Source, but it does not change prices, availability, or which seller they can choose.
* This is included with the existing Sales Agent connection because it corrects the seller's own inventory configuration. It is not a separate plan, metered capability, or way to differentiate sellers.
* You can upload several documents at once; each shows its own progress, and Save all saves the ones that are ready.
## 5.955.0 — October 8, 2026 at 3:47 AM UTC
* Certification creatives now go only to the source being certified. Other sources on the same storefront no longer receive them or get a creative review task.
* Documents you upload to the Library now arrive as Drafts, and your agent doesn't use or cite a Draft until you save it. Each Draft has a suggested name, category and description, labelled by where it came from: the file name, the format, or the content. Review it in the Library, then Save it to make it Live, or Discard it to delete the upload completely. If you ask your agent to save a Draft, it opens the Draft for you to review and save; it can't save or discard one itself. Rate cards and avails sheets are unchanged for now and still go through their price preview. The Library also filters by Live, Draft and Archived, and flags Live documents with no category.
## 5.953.0 — October 8, 2026 at 2:43 AM UTC
* Fixed a bug where saving a creative without an explicit template could report "could not be saved" even though the creative was saved successfully and attached to its campaign. Saving now succeeds normally in this case.
* Embedded sales agent 1.61.3: make media buy status follow GAM and close out ended buys that never went live, refuse to resume a GAM line item or approve its order when it has no budget cap.
## 5.952.0 — October 8, 2026 at 2:22 AM UTC
* Measurement records can now say which campaign, media buy and package they measure. Add `campaignId`, `mediaBuyId` and `packageId` to a record in `POST /advertisers/{advertiserId}/measurement-records`; each must belong to the advertiser, and a media buy fills in its own campaign when you leave `campaignId` out. Records come back with the same fields. A record without them still covers the advertiser as a whole. For now, a given outcome type, source, geo and time window holds one record, so a second record for the same window with a different media buy is refused with a conflict.
* More of your proposals now carry a written argument that cites your Library documents, instead of falling back to plain Library text. Every sentence of the argument is checked against the sources it cites, and that check never sees the buyer's brief. So the buyer's dates, budget, names and requests now appear only in "What we heard", and the thesis states what your own sources say about the products that fit the brief, without general premises such as "a launch needs a big first-day moment". The fact check itself is unchanged. Nothing to change on your side.
* The agent page no longer offers a transaction test that could not run for your account. A test that doesn't start now says so and gives the reason, instead of telling you its status needs checking. Certification runs automatically on live traffic.
* Meta media-buy creation now returns a specific, actionable reason when the adapter rejects a request before dispatch — the field, value, or account-readiness issue at fault — instead of a generic "correct the request and submit a new one" with no field or reason. This covers eligibility checks (Messenger, WhatsApp, Lead Generation, Sales Website Value), budget/currency/DSA checks, and geo-region/geo-city targeting resolution failures.
* Proposals no longer lose their written argument when the proposal writer plays back more than three points from the buyer's brief. "What we heard" now keeps the first three points, and the rest of the proposal is checked and written as usual. When a proposal still has no written argument, its diagnostic drop counts include the trimmed points under `section_claim_limit`. Nothing to change on your side.
* Updates, cancellations and delivery reports for a media buy with a managed sales agent are no longer refused when the caller uses a different AdCP version from the one the buy was created with. Each media buy now keeps the AdCP version it was created with for every later call, whatever version the caller uses.
* Embedded sales agent 1.61.2: apply buyer creative weights on GAM line items with MANUAL rotation, on every association path, refuse companion banners an ad server cannot traffic, before booking, on every adapter, translate click and domain macros to GAM without corrupting compiled trackers, validate creative rotation and refuse rotation an ad server cannot apply.
* Before a storefront rejects a buyer's `brand_id` as unlisted, it now reads the brand's registry record again if its stored copy is more than an hour old. A `brand_id` the brand has just added to its `brand.json` is accepted straight away instead of being rejected for up to a day. If the registry can't be read at that moment, the request goes through.
* While your storefront is being set up, an inventory source that is waiting on you now names the step in the rail, such as **Add products** or **Add credentials**. Before, it showed an in-progress indicator even though nothing was running.
* Your inventory source diagnostics now include `operationsTruncated` in the source health object. On your read it is always `false`, because it lists only operations that are still open, so there is nothing to change on your side. An attempt's `bodies.reason` can now also be `not_read`, which your read does not return today. When Apostra support looks at the same source, it also sees the operations that finished in the window, with each try's error code, so the operation ID you quote matches theirs. See [Diagnose third-party sales agents](/v2/storefront/inventory-sources/diagnostics).
## 5.951.0 — October 8, 2026 at 1:08 AM UTC
* Reviewer access for directory submissions is ready as soon as you request it. Choosing Request reviewer access on a Public listing now sets up the no-spend reviewer login straight away, instead of waiting for Apostra to activate it, and Open reviewer login appears right after. You can request reviewer access up to three times a day for each directory.
* Confirming your storefront's brand no longer fails with "the AAO registry is unavailable, try again" when the registry cannot read your brand.json live. You now see what is wrong with the file (it is missing, redirects to another domain, is over the 256 KB limit, or is not valid brand.json) so you can fix it. "Try again" is reserved for genuinely temporary failures.
* Storefronts now check a buyer's `brand_id` on `get_products`, `create_media_buy` and `sync_accounts` before passing the request to the seller. A `brand_id` is accepted when the brand's `brand.json` (or an owner-verified registry entry) lists it, when the buyer already holds a seller account on that storefront under it, or when the brand registry can't be reached. Otherwise the storefront returns a `VALIDATION_ERROR` naming the field and the brand's `brand.json`. Rejection is being turned on storefront by storefront; until then an unlisted `brand_id` is recorded and the request goes through.
* If Apostra sends you a private signup link for an Offer, you can now claim it. Sign in with the exact invited email as an administrator of your organization's billing account; claiming connects your organization to that exact Offer so you can review it in Plan & Billing. Claiming never accepts the Offer, starts paid service or charges a card. A link works once and expires after at most 30 days. A link that was revoked, replaced, used, or whose published terms changed is refused with its reason. A claim from a different email, or from someone who is not a billing administrator, is refused, and some conflicts go to the Apostra team for review. A browser page for opening these links is still to come.
* If your integration reads `tracking.impression_tracker_url` or `tracking.click_tracker_url` from a creative manifest, switch to `tracking.impression_tracker_url_template` and `tracking.click_tracker_url_template` before 9 November 2026, when the old fields are removed. The new fields appear on the get creative response (`GET /api/v2/buyer/campaigns/{campaignId}/creatives/{creativeId}`) and on the create creative response. Until removal, the old fields return identical values and are marked deprecated in the OpenAPI spec and docs. If you don't read them, nothing changes for you.
* The new names say what these values are: templates, not URLs ready to traffic. Each contains `{MEDIA_BUY_ID}` and `{PACKAGE_ID}` placeholders that only Apostra can fill, and the click tracker's `rurl` can also contain `{CREATIVE_ID}` and `{CAMPAIGN_ID}`. A seller's ad server cannot fill them. A template copied into an ad server by hand sends impressions and clicks with the placeholder text still in the URL, and they cannot be attributed to your campaign. Don't give a template to a seller to traffic. When Apostra syncs a creative to a media buy, it already sends the seller trackers with those ids filled, so syncing needs no change. Serve-time macros such as `{CACHEBUSTER}` can stay in the URL for the ad server to fill. The "Tracker templates" section of the creative object guide explains the difference.
* Editing a creative that is live on a storefront media buy no longer sends the seller placeholder ids. The update now reaches that seller once for each media buy the creative is assigned to, with that buy's media buy, package, creative and campaign ids filled in its trackers and landing page, so click and impression attribution keeps working after an edit.
* If your click-through URL contains `{PAGE_URL}`, shoppers who click your ad now reach your landing page with the address of the page where the ad ran, whenever the seller's ad server provides it. When the seller doesn't provide it, the token is removed rather than shown as placeholder text. Apostra uses the full address only to build the landing page link and stores only the page's host with the click. This applies to creatives synced to a seller after the release; nothing to change on your side.
* Fixed campaign execution failing with "no upstream media buy" whenever a creative was attached to a media buy on a connected ad-platform seller (Meta, Snap, Reddit, TikTok, and other adapter-connected sellers). The buy now launches normally whether or not a creative is attached.
* MCP calls refused because they had no valid session appear in your API call history (`GET /api/v2/activity/calls`) again, with the `X-Scope3-Activity-Id` response header. This covers a call sent without a session, with an unknown session, or with another principal's session; for example, a `tools/list` sent before `initialize`. If the call also carries an invalid `X-Scope3-Run-Id` or workflow header, the response reports that header error first, as it does for every other call. Nothing to change on your side.
* Murph now reads the whole of long documentation pages. It used to see only the first 20,000 characters of a page, so on 65 of our longest pages, including the glossary, source diagnostics, the tool reference and authentication, it answered later sections from short search snippets. It now fetches the exact section a search points to, and pages through long pages with an outline of their sections. Answers about anything near the end of a long page, such as how to verify our signed requests, now come from the full text.
* The published response schema for listing your account grants on a Storefront (`GET /api/v2/buyer/storefront-accounts` and the `list_buyer_storefront_accounts` MCP tool) now allows additional fields in `ext.interchange`, and `ext.interchange.advertiser_activation` is now optional there. Responses are unchanged today: when `advertiser_activation` is returned, it has the same fields and values as before. If you validate responses against a client generated from the schema, regenerate it so it accepts fields we add to `ext.interchange` later, and treat `advertiser_activation` as possibly absent. Nothing else changes on your side.
* A Demo Storefront's synthetic managed source now shows its inventory sync as complete instead of asking you to run a first sync. The synthetic source has a fixed catalog and no ad-server inventory sync, so there was never a sync to run or retry.
* If you read a creative through V3 `get` with `kind: "creative"` and use `resolvedClickUrl`, switch to `clickUrlTemplate`. `resolvedClickUrl` is removed in this release with no alias, because V3 is in preview. If you don't read that field, there is nothing to change on your side.
* `clickUrlTemplate` is the creative's click URL with the click-URL parameter additions that apply to it, and with `{CREATIVE_ID}` always filled in. `{CAMPAIGN_ID}` is filled in when the creative belongs to one campaign. An advertiser library creative that isn't tied to one campaign keeps `{CAMPAIGN_ID}` as a placeholder. `{MEDIA_BUY_ID}` and `{PACKAGE_ID}` always stay as placeholders, because they differ for each media buy and package. Apostra fills them when it syncs the creative to a media buy.
* That is why the field is called a template. If you copy it into a seller's ad server by hand, the click URL will contain the literal text `{MEDIA_BUY_ID}` and `{PACKAGE_ID}`. Serve-time macros such as `{DOMAIN}` and `{CACHEBUSTER}` also stay in the URL for the ad server to fill.
* Syncing needs no change. Apostra already sends sellers click URLs with these ids filled in. The `clickUrl` field, which is the URL as you saved it, is unchanged. In the creative library, the destination panel row that was labeled "Delivered as" now reads "Click URL template".
## 5.950.0 — October 7, 2026 at 11:20 PM UTC
* Products a storefront composes from a brief now list the targeting you can set on a package, as `overlay_support`. Each one carries the declaration of the inventory product it is built from, so brief results and wholesale results describe the same inventory the same way. A product whose inventory declares no targeting support still lists none.
* Update billing info now documents that your organization's payment terms are writable. An organization admin sends `paymentTerms` (`net_15`, `net_30`, `net_45`, `net_60`, or `net_90`) to `PUT /api/v2/billing/info`; no approval step follows and the change is audited. It also explains how terms reach each seller account: consolidated-billing accounts ask for the organization's terms, while direct-billed and sandbox accounts carry none and get the seller's default.
* Buyer setup docs now state that payment terms can be requested for each seller account. Set `payment_terms` (`net_15`, `net_30`, `net_45`, `net_60`, `net_90`, or `prepay`) on an entry in `sync_buyer_storefront_accounts`, or omit it to accept the seller's default. The organization-wide choice is separate and takes `net_15` to `net_90` only. The V3 hosted-agent tools set only the organization-wide choice.
* When your organization can't add another account, Organization → Accounts now says why: the plan doesn't include additional accounts, no longer includes them, or isn't in effect yet. Before, all of these showed "Account creation is unavailable right now." Organization inspection through MCP (`manage_organization`) now reports the same answer as account creation, instead of reporting that an organization without account capacity could add accounts.
* Organization members who are not administrators now see the organization's accounts they belong to when they open the organization, instead of an empty buyer dashboard that showed no advertisers or campaigns. Open an account from the account switcher to work in it.
## 5.949.0 — October 7, 2026 at 10:05 PM UTC
* An advertiser now carries a brand ID only when the brand publishes it: the brand's `/.well-known/brand.json`, or a registry entry the brand owner verified, must list that ID. A brand ID found only in community or enriched brand data is not stored, and the advertiser is identified to sellers by its brand domain alone. Interchange also rechecks each stored brand ID about once a week. If a brand stops listing an ID that sellers already know, Interchange keeps sending it so existing seller accounts stay intact, and flags it to Scope3 operations. Nothing to change on your side.
* Third-party impression trackers that use `{DOMAIN}`, `{PAGE_URL}` or `{APP_BUNDLE}` now run on Google Ad Manager sellers. Before, a tracker with any of these macros was left off the creative when it went to a Google Ad Manager seller. Now each macro is converted to Google's own macro: `{DOMAIN}` to `%%SITE%%`, `{PAGE_URL}` to `%%PATTERN:url%%` and `{APP_BUNDLE}` to `%%TAG_PARAM:msid%%`. Google fills `{APP_BUNDLE}` only on video ad requests from apps (not on iOS or tvOS), so expect it to be empty elsewhere. A tracker that uses `{CLICK_URL}` is still left off, because Google would count a click every time the pixel loads. `test_creative_macros` shows the same conversions when you choose Google Ad Manager as the target.
* MCP requests now continue using the account selected with `switch_account` or `customer_switch` on later turns.
* When the web app fails to download its code while starting, it now retries once more after a short pause before giving up. If it still can't load, the message says the page couldn't finish loading and offers a Reload button, rather than pointing at a configuration problem.
* Buyers can stage media buys when a wholesale catalog product has an obsolete source-less duplicate alongside its sourced route.
* Delivery reads now return canonical revision finality and data-through when evidence exists, while clearly reporting unavailable evidence when it does not.
* `get_products` and `request_proposals` no longer hide a request that Apostra's own pre-dispatch validation refused (for example, a brief that states a hard age targeting requirement). That seller's `storefront_results[]`/`perSeller[]` row now reports status `failed` with a new `local_refusal` outcome carrying the original `code`, `message`, and a `suggestion` for correcting the request, instead of silently completing as an empty, successful catalogue. A seller refused this way is no longer counted as queried or searched.
## 5.948.0 — October 7, 2026 at 9:09 PM UTC
* Syncing a campaign's creatives to its storefront media buys now fills each media buy's own ids into the Apostra click and impression trackers and into the landing page they redirect to. Before, this sync sent `{MEDIA_BUY_ID}`, `{PACKAGE_ID}` and `{CREATIVE_ID}` as literal text, so clicks reached your landing page without the ids your attribution joins on. `{PACKAGE_ID}` is filled when the media buy has a single package and left empty otherwise.
* Reject impossible campaign and media-buy flight dates with a correctable validation error. `get_status` and flight-bearing saves now return UTC `serverTime`; refresh `get_status` before each relative-date calculation rather than reusing connection or save-receipt time.
* Managed FreeWheel connections now accept a buyer's HTTPS VAST tag of any AdCP version, from 2.0 to 4.3, with no per-publisher enrollment. Previously only VAST 2.0 tags reached FreeWheel, and only on connections enrolled one by one; other tags failed creative sync with `destination_profile_not_certified`. The tag is sent to FreeWheel unchanged, and if FreeWheel refuses it, FreeWheel's own validation error is returned on the creative result. Hosted video files still need a VAST tag for FreeWheel.
* Plan & Billing now shows how many accounts a plan allows, such as "Up to 100 accounts under this organization" on Business Manager, instead of an empty features line.
## 5.947.0 — October 7, 2026 at 8:24 PM UTC
* Adopt a direct provider's active media-buy status when a materialised route is still pending.
* Sellers whose agents speak A2A can now discover Apostra's request-signing keys from our agent URL. `https://api.apostra.com` (and `https://api.interchange.io`) serves an A2A agent card at `/.well-known/agent-card.json` and answers `get_adcp_capabilities` over A2A without credentials, with the same `identity` block it already returns over MCP and as a plain JSON `GET`. An A2A verifier that follows AdCP's key discovery now finds our `brand.json` and signing key the same way an MCP verifier does.
* Automatic product selection now ranks sellers using only your own account's completed buys with each seller. Other buyers' results no longer move your ranking. Where you have no completed buys with a seller, its ranking rests on its product history alone, as it already does for a seller with no record.
* Billing links open Plan & billing on the tab they name again. For a live seller, a billing link from an email, a setup step or a plan offer opened Account settings → Plan & billing and then switched it to Overview, and could leave you on the home page with the pane closed. The pane now stays on the requested tab, including after you answer the Rate Card prompt with Not now; closing it returns you to your usual home. The admin and storefront billing links can now name a tab as well. Nothing to change on your side.
* A buyer agent with **Read** access to an advertiser can now run product discovery (`get_products`, `discover_products`), resolve a brand (`resolve_buyer_brand`) and preview creatives (`preview_creative`) for that advertiser through MCP `api_call`, and run discovery and brand resolution through REST. Before, `api_call` refused all four and REST refused discovery and brand resolution, treating them as changes because they are sent as POST. A hosted agent acting for a teammate with a Basic role gets the same reads. A catalog activation plan preview through `api_call` still needs **Read and write**.
* Saving a catalog activation plan through REST (`POST .../catalogs/{catalogId}/activation-plan/preview` with `save: true`) now needs **Read and write** access to the advertiser. A buyer agent granted only **Read** gets `403 ACCESS_DENIED` and no plan is saved. Before, REST treated every preview as a read, so a Read-only agent could save a plan. Previews that do not save still need only **Read**.
* If your Buyer Account's operator identity was locked by an advertiser before you chose its account scope, you can now confirm it as the whole operator yourself with `save_buyer_operator`, using the same operator domain. Previously this could only go through support, and the save failed. Once the scope is confirmed, new media partner connections can set up your advertisers' accounts. A specific operating unit or a different domain on a locked identity still goes through support.
* Your Scope3 contact can now connect a shared Slack Connect channel to your account by asking Apostra in the channel, whichever organization created it. Until the channel is connected, Apostra answers there from public documentation only.
* `request_proposals` results are readable once a named seller answers. Each seller outcome includes the seller's display name when it resolves, but the tool's declared output schema didn't list that field, so MCP clients that check responses against the schema got an error instead of the per-seller outcomes. The schema now declares `sellerName`.
* Activating an inventory source that a Sales Agent serves, before that Agent's connection is ready, now returns a clear `409 CONFLICT` with reason `agent_connection_not_ready` instead of an internal server error. The connection is ready when the Agent's production certification is current and the connection passes its authorization, validation, health, and commercial checks. The source stays in its current status. Check certification on the Agent's Test & certify page and the connection on the source's diagnostics, then activate the source again. AAO compliance remains advisory and does not block this step.
* `create_media_buys` now advertises the request shape it actually accepts. The tool schema lists the inline `campaign.create` fields (`advertiserId`, `name`, `flightDates`, `budget` and `mode: "discovery"`) and separate, closed shapes for proposal and product selections. A request that matches the advertised schema now passes validation. When a request is rejected, the error names the missing `campaign.create` fields instead of `campaign: Invalid input`, and an unknown key on a strict object lists the keys that object accepts.
* `campaign.create.advertiserId`, `catalogId` and each `storefrontIds` entry now take only a positive integer or a string of digits, such as `12345` or `"12345"`, and the tool schema publishes exactly that rule. These fields used to coerce any value JavaScript's `Number()` could convert, so `true` became ID 1 and `[7]` became 7. Values like `true`, arrays, `""`, `"0"`, `"01"` or `" 12"` now get a 400 `VALIDATION_ERROR` that names the field and the accepted form. The same rule applies to `POST /media-buys/batch`, which `create_media_buys` calls. `POST /campaigns` is unchanged.
* Fixed storefront discovery so ordinary audience and content-suitability wording does not block discovery. If brief reading is temporarily unavailable, clearly stated age requirements still prompt buyers to use structured age targeting.
* Return a clear provider-read error instead of an internal server error when live campaign delivery cannot be read.
* Snap and Meta rejected media buys whose optimization goal names one of your own event sources. For buyers enrolled in the rollout, Apostra now sends the seller the pixel matched to that event source on the seller account (a Snap Pixel, Meta dataset or TikTok pixel) instead of your event source ID. If matching is still in progress, the buy is not sent yet; it is sent automatically once the pixel is matched, and the launch response lists it with `SELLER_OBJECT_SYNC_PENDING`. No action is needed while it waits. The buy fails instead with `SELLER_EVENT_SOURCE_DESTINATION_MISSING` when the seller account has no pixel or not the one you named, `SELLER_EVENT_SOURCE_DESTINATION_AMBIGUOUS` when it has several and you named none, or `SELLER_DATA_SHARING_NOT_PERMITTED` when the **Conversion events** switch is off for that seller in Connections. Create the pixel in the platform's console or name one for each seller account with `sellerAccounts` on the event source, or turn the switch on, then create a new media buy. A seller that cannot use event sources, such as a managed seller agent or an AdCP agent outside Apostra, fails the buy with `SELLER_OBJECT_TYPE_UNSUPPORTED`. Seller-managed event sources and platform built-in events pass through unchanged, and the media buy keeps showing your own event source ID.
* If you administer a Partner organization with the paid or beta right to operate agents for clients, you can now check the health of any client source your sales agent powers in the source diagnostics widget, without a debug invitation. Call the `open_partner_source_diagnostics` MCP tool on the Storefront surface with the source's `bindingUid` and, optionally, `windowHours`. The widget shows the source's identity, health by operation, diagnosis and open operations. It is read-only: no finding offers a next step and there are no write outcomes. Findings owned by the client or Apostra show their code and owner with generic text, and an operation waiting on the client shows that it waits and since when, not what the client has to decide. A line says whether the client's debug invitation is active and until when.
* Picking another window in the widget re-reads the source. The tool is listed only for administrators of organizations with that right.
* Nothing changes for clients, and there is nothing to do on your side. The REST read `GET /api/v2/provider/account/connections/{bindingUid}/source-health` was already available; this release adds the widget and the tool. See "Read one client source's health" in the Partner account docs.
* `save_event_source` now accepts up to 50 seller accounts per event source in one save, up from 20, and at most 200 seller accounts across all event sources in one save. A save over 200 is refused with `VALIDATION_ERROR` before anything is saved. Each entry still names a connected seller account and the pixel to use on it.
* Products from a managed sales agent keep the demographic targeting the seller declares, such as supported age ranges, in its AdCP 3.2 shape. Buyers reading AdCP 3.2 product capabilities now see it for these products, as they already did for other sellers.
* On Google Ad Manager storefronts, a creative that a buyer assigns to specific placements now serves only on those placements. Before, the placement choice was accepted and then ignored, so the creative served across the whole package. If a storefront's ad server can't route creatives by placement, a buy that asks for it is now refused up front instead of serving everywhere. A media buy that fails while its ad-server order is being set up is now closed out cleanly instead of being left looking active. Signals a seller marks as exclude-only can now be used to exclude an audience in the newer signal-targeting format (AdCP 3.2), and are refused if a buy tries to include them. No action is needed.
* Embedded sales agent 1.61.0: audit MCP credential headers that adcp 9 will refuse.
* Embedded sales agent 1.61.1: send the buyer-visible status on account.status\_changed webhooks.
* Campaign execution now reports attempted, successful, and failed media buys separately. `mediaBuysExecuted` keeps its published attempted-count meaning; use the new `mediaBuysSucceeded` count to identify successful executions. Proposal requests also preserve the local brief-validation reason when it blocks a round.
* Country-specific advertisers can now hold separate seller accounts under the same brand on AdCP 3.2. When Apostra syncs an account to a seller that speaks AdCP 3.2, the brand now carries the advertiser's countries (for example `["NL"]`), because AdCP 3.2 counts countries as part of the account key. Sellers on AdCP 3.1 or earlier still receive only `domain` and `brand_id`, and an advertiser with no country scope still sends none. Storefronts hosted on Apostra now accept `brand.countries` in `sync_accounts` for AdCP 3.2 complete-key accounts: two requests that differ only by countries create two seller accounts, while the same countries in a different order or case resolve to one. A request that sends `brand.countries` without negotiating AdCP 3.2 is still refused with `adcp_3_2_account_identity_not_supported`, and the former `seller_account_country_identity_not_supported` error no longer exists. Existing seller accounts and advertisers do not change, so nothing to do on your side.
* Offline conversions sent to Meta are now recorded as "other" instead of "website", so Meta no longer reports them as website conversions. Meta accepts these events for up to seven days. An older offline or "other" event now comes back as its own entry in `partial_failures` with code `META_EVENT_TOO_OLD`, and the rest of the batch is still sent. Nothing to change on your side.
* When your agent rejects requests we signed, source diagnostics now say so. Previously the source was described as having "stopped responding, so its inventory can't sell until it reconnects", or as an error diagnostics "could not classify". The diagnosis now:
* reads that your agent rejected our signed requests;
* shows your agent's own reason in Technical details;
* tells you to fix your signature verifier;
* links the "Our signed requests rejected" troubleshooting section.
* The API response shape is unchanged.
* When your agent rejects a request we signed, your source diagnostics now show your agent's own reason. Previously they showed a generic authentication message, and at one point told you to add an OAuth token. The diagnosis records your agent's `request_signature_*` error code when it sends one, otherwise the text of its 401 response (code `signed_request_rejected`). This covers discovery, catalog refresh and the discovery test. A new troubleshooting section, "Our signed requests rejected", explains how to fix your verifier, most often by resolving our signing key from our agent URL. Buyers still see only that the source rejected the request, never your agent's text.
* When your agent rejects a media buy (create or update) or an account sync that we signed, your media buy timeline now shows your agent's AdCP request-signing error code (or `signed_request_rejected` when it sends none) instead of a generic authorization failure or no code at all. We also store the text of your agent's 401 response with each rejected attempt, so our support team can see your agent's own reason. Buyers still see only that the source didn't authorize the request.
* Wholesale catalog duplicates without a source are no longer retained when a sourced route is available.
* A newly created account, such as a new Demo Storefront, now appears in the account switcher of your other tabs as soon as you reload them. Before, it could stay missing for several minutes.
* The "Verifying our signed requests" guide now answers the questions sellers hit while building a verifier:
* **How you know a request is from Apostra.** Nothing in the request names the signer. You set us up once as agent `https://api.apostra.com`, and the signature's `keyid` must resolve to a key in that agent's JWKS.
* **`keyid` is a bare key ID,** never a URL.
* **Two separate checks.** The signature proves who sent the request. The brand's `authorized_operators` says whether an agency may act for that brand. The agency named on an account never lists or vouches for the buying agent.
* **Which direction.** Verifying our requests needs no JWKS or `brand.json` of your own. Signing the webhooks you send us is covered separately.
* **Check your setup.** `npx @adcp/sdk@latest resolve https://api.apostra.com` shows each step of key discovery against our live agent.
* The identity documents page also clarifies that a `brand.json` listing an agent belongs to whoever runs that agent, not to the agency on an account.
## 5.946.8 — October 7, 2026 at 2:34 PM UTC
* Buyer MCP connections that select an advertiser can call every buyer tool their role allows again. Billing account, buyer readiness, buyer activity, storefront account and media-buy update calls no longer fail with "Tool is not available for this advertiser" when the connection names an advertiser. Your role and your access to that advertiser still decide what each call can read or change. Nothing to change on your side.
## 5.946.7 — October 7, 2026 at 1:54 PM UTC
* The V3 `search` for `wholesale_product` with `include: ["executionResources"]` now lists options for products from Meta, TikTok, Snap and LinkedIn. It used to answer "Wholesale product not found for this advertiser" for every product from those sellers, even when the `productId` came straight from your search.
## 5.946.4 — October 7, 2026 at 12:29 PM UTC
* Signals you've set up as a fixed key-value pair, or as a combined signal (several criteria that must all match), now work on media buys sent in the newer signal-targeting format (AdCP 3.2). Before, those buys were refused. Buys using your audience-segment and Google Ad Manager key signals work as before. No action is needed.
## 5.946.3 — October 7, 2026 at 12:03 PM UTC
* Approved creatives without an explicit format option can now use the media buy's exact booked product format when the seller advertises AdCP 3.1, including alongside 3.2. Existing creatives can be delivered without being recreated, while modern routes without complete exact booking evidence keep their existing canonical format declaration.
## 5.946.2 — October 7, 2026 at 11:42 AM UTC
* `save_media_buy` on the v3 agent surface now accepts `pageId`, `instagramActorId`, `welcomeMessageFlowId`, `leadFormId`, and `appId` on `products[]` when staging a new media buy. These are the same provider resource choices a product's execution readiness can report as required (which Facebook Page, Instagram account, Messenger/WhatsApp welcome flow, lead form, or app to use); previously only `pixelId` was available on v3, so buyers who needed one of these had to fall back to v2 `create_media_buys`.
## 5.946.0 — October 7, 2026 at 10:46 AM UTC
* Meta product discovery now returns only the campaign products Apostra has verified end to end. Fifteen products that are still being verified no longer appear in standard discovery: Leads (Instant Forms, quality leads, Messenger, Instagram, WhatsApp and Calls), Messenger and WhatsApp conversations, Instagram profile visits, event responses, the three App Promotion products, catalog and Shop sales. A brief that asks for one of them returns a clarification instead of a plan. The Meta compatibility guide shows each product's current status.
* The V3 `search` for `wholesale_product` can now list the options behind a product's execution readiness. Add `include: ["executionResources"]` with the product's `productId` and a `resourceKind` such as `platform_identity`, and each row is one option you can choose, such as a Facebook Page or pixel, paged with `nextCursor`. The first call asks the seller and may ask you to poll until the list is ready.
## 5.944.0 — October 7, 2026 at 6:21 AM UTC
* Poll a pending Apostra API write by receipt ID to learn its operation state and next step.
## 5.943.1 — October 7, 2026 at 5:59 AM UTC
* The account switcher in the top bar now only switches accounts. It no longer shows an organization heading, an organization settings gear or an Add account button, and it appears only when there is an account you can open other than the one you are in, or an invitation waiting. Choosing another organization's account opens that account, never a different one picked for you. Organization settings stay in Account settings → Organization. Administrators of a standalone account now add accounts from a new Accounts item in Account settings, which explains that adding one creates an organization for both accounts. No action is required.
## 5.943.0 — October 7, 2026 at 5:21 AM UTC
* When you connect an external Sales Agent, Apostra now links it to the one registered Agent with the same endpoint or origin when that Agent is ready to accept a new Source. If there is no single ready match, choose an Agent from the Source page instead. Existing Agent links do not change when you update an endpoint. This only changes seller setup; buyers do not see a change, and it does not change plans or billing.
## 5.942.0 — October 7, 2026 at 3:00 AM UTC
* Account admins can now create inbound email addresses for briefs and reports, optionally for one of their advertisers, and rotate or revoke them without contacting support. Each address has a readable prefix and an unguessable suffix. Mail to an unknown, rotated or revoked address is refused. An address tells Apostra which account an email belongs to, never who sent it. Mail that arrives is recorded privately in your account; Apostra does not read or act on it yet. Inbound email is in a limited pilot: today only enrolled accounts can create or rotate addresses. Any account admin can list and revoke them. For other accounts, creating or rotating an address through the API or MCP returns `FEATURE_NOT_ENABLED`. See [Inbound email addresses](/v2/concepts/inbound-email-addresses).
## 5.941.0 — October 7, 2026 at 2:21 AM UTC
* If you reload a tab that was switched into an account you can no longer open, for example one that has been archived, the app now takes you back to your home account instead of showing an empty page for the old account.
## 5.940.1 — October 7, 2026 at 1:39 AM UTC
* An image Creative Session draft that names no size or aspect ratio now generates a square 1024 by 1024 image, so `generate_variants` no longer fails on it; the saved plan's `params` shows that size. If a request is refused before any provider is contacted, the result now says so, says no provider charge occurred, and says how to fix it, for example by setting `params.width` and `params.height`. Nothing to change on your side. A draft that names only a width or only a height is still refused until it names both, and drafts saved before this change keep their saved request.
* Buyers who have no account yet with a seller get that seller's live products again. We create the account with the seller automatically, as before, and reuse the seller's account ID for later requests.
* Every storefront listed in the account switcher now shows its status line. Before, a storefront that appeared in the list because it shares your organization, without your being a member of it directly, showed no status. This affected a new Demo Storefront, for example.
## 5.940.0 — October 7, 2026 at 1:14 AM UTC
* Voice drafts now recognise the language and length you ask for in the brief (for example "a 15-second Japanese radio spot"), so they no longer show false "no language" or "no duration target" warnings. Nothing to change on your side.
* When you ask for proposals without naming sellers or setting a seller list on the campaign, Apostra now contacts only the media partners turned on for the campaign's advertiser. Partners that are off are no longer contacted or reported one by one. If no media partner is on for the advertiser, the request ends with a message saying so and where to turn one on: open Media Partners for that advertiser, open a media partner, choose Controls, then set Availability to On. Nothing to change on your side.
* Your connected Sales Agent's product mode now follows its declared AdCP buying modes after a capability refresh, so a wholesale declaration no longer needs a staff-set mode.
* Execute media buys with hosted-video creatives that match a seller's selected format option.
* Storefront proposal requests with composed products now keep the proposal record needed to attach a pitch. Nothing changes in the products buyers receive, and sellers do not need to take action.
* A seller whose only open setup item is an ad-server go-live certification (such as the Google Ad Manager write canary or the FreeWheel booking certification) now stays visible to buyers. Buyers can search its products and add them to a buy they are planning; booking with it is refused with a message saying the seller has not finished going live, instead of saying the seller is off for the advertiser. Sellers see the certification as its own readiness item, `esa_go_live_certification`, and booking opens once it passes. Discovery now also names a seller that cannot be searched for any other reason as not available to buyers, rather than as off for the advertiser.
* Library Materials now support Draft review, including seller-confirmed descriptions and the `media_kit` category. Existing uploads continue to become Live; a later Library update will add upload Draft review. V3 search: the Material filter `archived` is replaced by `materialState` (`live`, `draft` or `archived`); update calls that used `filter.archived`.
* Administrators of a Partner with the paid or beta right to operate agents for clients can now read the health of any client source their agent powers, without waiting for the client to invite them to debug it. Call `GET /api/v2/provider/account/connections/{bindingUid}/source-health`. It works as long as the client still authorizes your agent for that source, and the connection does not have to be activated, so you can diagnose a source whose validation is failing.
* You get the same source health object the client sees (health by operation, diagnosis, open operations), with these limits:
* No finding offers an action. You act through your own agent, not on the client's behalf.
* A finding the client or Apostra owns keeps its code, owner and severity, but its text is generic.
* An operation waiting on the client shows that it waits and since when, not what the client has to decide.
* `writes` is null, as in the client's own read.
* Request and response bodies stay withheld unless the client has an active debug invitation for that exact source.
* Nothing changes for clients, and there is nothing to set up on your side. This is an API read for now, with no screen for it yet. See "Read one client source's health" in the Partner account docs.
* Embedded sales agent 1.59.1: publish each undeclared product with the targeting its ad server applies, validate overlay\_support with a resolver of its own on each request thread.
* From October 8, 2026, wholesale products you create on a managed sales agent, in Inventory Components or through your storefront agent, must declare their AdCP `channels`, such as `display`, `olv`, `ctv`, or `streaming_audio`. A create without them is refused, and the error names the field to add. The same date applies to avails feed rows: a row with no `channel` and no default channel on the request is refused with `CHANNEL_MISSING`. Until then, both are saved with a warning. Products and rows you already have keep working, and editing an existing product is not affected.
* Products from third-party sales agents are not refused for missing channels. The **Product channels declared** warning stays a warning, and certification never fails an agent for missing channels alone. This replaces our earlier note that channels would become required for external agents' products. Whether they become required is for AdCP to decide: a proposal to require `channels` on every Product in AdCP 4.0 is open as [adcontextprotocol/adcp#7869](https://github.com/adcontextprotocol/adcp/issues/7869).
* Fixed creative sync and media-buy execution to reuse a seller's resolved account ID and stop before launch when account linkage is missing.
* A creative delivery no longer stays "processing" after every seller it was sent to has accepted or rejected it. Interchange now checks for these deliveries every few minutes, sets each one to its final result, and sends your webhook the usual final update. Deliveries still waiting for a seller's review are unchanged and stay open until the seller decides. Nothing to change on your side.
## 5.939.0 — October 6, 2026 at 11:43 PM UTC
* V3 `save_media_buy` now honours `idempotencyKey` on flight and budget updates, as its description promised. Retrying an update with the same key and the same request returns the first result instead of applying the change again (unless the buy has changed in between, in which case the retry is refused against its current state), which matters when a slow response is retried while the seller is still confirming. The same key with a different request is refused, and a key still in use returns a retry-after time.
## 5.938.1 — October 6, 2026 at 11:21 PM UTC
* Launching a media buy now stops before seller submission when every selected creative is excluded, explaining the creative and competing format options so the buyer can select an explicit format option.
* A source's open operations, shown in its source health, no longer list creatives you have not approved yet. No source receives a creative before your review, so these creatives were listed against a source that had never been asked about them, some since July. They still wait in your creative reviews, and they appear on the source once you approve them and delivery starts. Nothing to change on your side.
* V3 `save_media_buy` can now change the budget of a media buy that is already live. Send the line item's new total, including what it has already spent, in `products[].budget` (or a top-level `budget` on a buy with one line item). Pacing periods that have ended keep their budgets, the open periods share the rest in their current proportion, and the change waits for the seller to confirm as before. Before, a live buy's budget change was refused once its packages were deployed. A total below what the ended periods hold plus what the open periods have spent is refused with that amount. When the same product appears as more than one line item, name the one to change with `products[].lineItemRef`.
## 5.938.0 — October 6, 2026 at 10:51 PM UTC
* Third-party sales agents now receive a debug ID on Interchange requests, so support teams can match a seller log line to the matching Interchange request.
* A creative delivered from the Creative Library to a sales agent is now sent once after the seller accepts it. Before this fix, routine background status checks on a seller's sales agent could make Interchange send an already-accepted creative to that sales agent again, about every 15 minutes, and send the buyer's webhook a fresh status update each time. A creative is still sent again when the sales agent it goes to actually changes, for example a new address or a different sales agent. Nothing to change on your side.
## 5.937.1 — October 6, 2026 at 9:58 PM UTC
* Voice clips for Chinese, Japanese and Thai scripts are now checked against the right expected length. Previously a long script in one of these languages could be accepted even when the generated clip was cut off after a second or two; those clips are now rejected as too short, like they already were for English.
## 5.936.0 — October 6, 2026 at 9:33 PM UTC
* Embedded sales agent 1.58.1: send each caller get\_adcp\_capabilities targeting in the shape its AdCP release defines.
* Discovery now requires exactly one existing seller account. If no exact account exists, or several accounts match, create, link, or resolve the seller account before retrying. Later account-scoped requests reuse its opaque account ID.
* On an inventory source's diagnostics page, choose **Source health** to see one source's health in one place: which seller account, source and Agent you are looking at and whether buyer requests reach it; the status of each AdCP operation Apostra performs against it, with its last success and last failure; each finding with its severity, who has to act and the next step; and the operations still waiting at the source, with who they are waiting on and since when. The window selector sets the period the counts cover. Nothing changes on your side, and the existing overview, setup and technical details are unchanged.
## 5.935.2 — October 6, 2026 at 8:34 PM UTC
* When the agent on your agent phone line answers a text, its reply is now added to the same conversation record as the text it answers, once for each reply. Like the texts, the reply is recorded without its words or the number it goes to. A reply to a text received before texts were grouped into conversations is not added. The record is still not shown anywhere and the agent does not read it, so nothing changes in how or when the agent replies.
* When a seller turns down a media buy after you submit it, or it never goes through, your `create_media_buy` task now always ends as `failed`. Before, a brief fault on our side at that moment could leave the task pending for good. Apostra now keeps retrying that update in the background until it lands.
* Nothing to change on your side.
## 5.935.1 — October 6, 2026 at 8:10 PM UTC
* A Scope3-funded creative preview that is refused before it reaches the provider now always gives its daily preview unit back, even if the database hiccups at that moment. Before, a brief database error could leave the refused request counted against your account and the global daily preview caps until the next UTC midnight.
* Retrying the same generation after such an error now completes the refund exactly once. Nothing to change on your side.
* Apostra pricing and documentation now use Enhanced Reporting consistently for the connected-account reporting activity at 4 IUs per eligible account and billing period.
## 5.935.0 — October 6, 2026 at 7:26 PM UTC
* Fixed legacy creative re-syncs so they can add the format route required for delivery without changing any other stored sync identity.
* When you update a media buy that a seller is already running, a property-list or collection-list reference you set on a package is now sent only if that seller declares it can apply that exact list field. If the seller doesn't, the update stops with a validation error naming the package, the list field and the seller, and nothing is changed or sent. To proceed, remove the reference from the package or move it to products whose seller declares that field. Draft buys are checked when you launch them, as before.
* Saving a generated creative to your Library from the variant gallery still shows "Saved to your Library." even if the host can't record the matching chat activity, and a reload never posts that activity twice. We now report a failed or timed-out activity write so we can see it happening.
* V3 `save_media_buy` can now change the end date of a media buy that is already running. Send the buy's current `flight.startAt` unchanged with the new `endAt`, and the start is left alone. Before, every way of sending a start was refused once the buy had started, so a running buy could not be extended or ended early through V3. A different start on a running buy is still refused.
* The docs now have a Get support page (`/v3/support`) explaining how to ask the in-product assistant and how to reach the support team, with what to include in a request. Apostra's ChatGPT app listing links to it as its support page.
## 5.934.0 — October 6, 2026 at 6:40 PM UTC
* Texts sent to your agent phone line are now kept for 30 days. After that, Apostra deletes a text's words and the number that sent it, except that a text the agent is still working on or waiting to answer keeps them until the agent has finished with it. Only the record that the text arrived, and what happened to it, remains. Ordinary texts from one number to one line are also grouped into a single conversation record for your Seller Account. The record holds neither the words nor the number. Opt-out, opt-in and help keywords are left out of it, and so is a text that arrives after its number has opted out. A text recorded before its number opted out stays in the record even if the agent never answers it. The record is not shown anywhere yet and the agent does not read it, so the agent still answers each text on its own, and nothing changes in how or when it replies.
* Fixed: a buyer agent calling `list_campaigns` or `get_campaign` through the MCP `api_call` tool could not read the campaigns of an advertiser another organization delegated to yours. The call was refused with `agent_read_only` even when the delegation included `campaign.read`. Both reads now work through MCP as they already did over REST, and each campaign read this way carries `delegatedBy`. The agent still cannot change anything on the delegated advertiser. Limited beta: available to organizations with the advertiser delegation entitlement.
* When a seller's sales agent pauses a media buy create or update for more input (the AdCP `input-required` status), Apostra now fails it as soon as it sees the pause, instead of leaving it pending for up to seven days. The input the agent asks for is Apostra's to give, and Apostra cannot supply it yet, so waiting would never move it.
* **Buyers:** a create the agent pauses fails straight away, either in the `create_media_buy` response or as a `failed` task if the pause comes later. Its notification carries `errorCode: platform_error`. A paused update fails the same way and leaves the buy unchanged.
* **Sellers:** on your media buy list, the buy shows `errorCode: source_input_required`, and its next action names the platform, so there is nothing for you to do. It no longer appears as open work in Pending Operations or in source diagnostics.
* Nothing to change on your side.
* Embedded sales agent 1.58.0: each product's declared targeting is now checked when a buy is booked or updated, in report-only mode — what would be refused is logged and the buy books as before, until enforcement is switched on per seller. Products with no declared targeting are not checked. get\_products filters by required\_overlay\_support. The sales agent now advertises only the targeting each ad server applies, and refuses targeting an ad server can't apply instead of dropping it. Also: live GAM geo targeting in the GAM snapshot, the geo code systems each adapter reads for metro, postal and place targeting, an auto-provisioning account reported as active rather than pending\_approval, and GAM's legacy time zone ids resolved in the production image so read-backs compare flight dates.
## 5.932.0 — October 6, 2026 at 4:37 PM UTC
* The V3 `search` for `wholesale_product` can now tell you whether each product can be bought on the advertiser's connected ad account. Add `include: ["executionReadiness"]` and each row carries an `executionReadiness` summary when its seller reports it (currently Meta, TikTok, Snap and LinkedIn): an overall status, plus each account resource the product needs, such as a Page or pixel, with what's missing, the option already chosen, and how many options there are. Without the include, rows are unchanged.
## 5.931.0 — October 6, 2026 at 4:09 PM UTC
* Pending Operations now says who each pending item is waiting on, and lists more of what is waiting at your sources. Every approval, creative review, buy awaiting its source, and manual modular-source task carries `waitingOn`: who must act next (you, the sales agent's operator, the buyer, or Apostra), since when, and the exact thing being waited for, such as your approval or the task id the sales agent returned. Behind Apostra's managed sales agent, a buy held at the source is shown as waiting on you, because the review it is waiting for is in your own ad server. Two new groups list changes to live buys that a source has not applied yet (`updatesAwaitingSource`) and approved creatives a source has not accepted yet (`creativesAwaitingSource`). The Pending Operations Page shows each item's "waiting on … for …" line and the two new groups. Nothing to change on your side.
## 5.929.0 — October 6, 2026 at 3:29 PM UTC
* When you launch a media buy, a property-list or collection-list reference you set on a package yourself is now sent only to a seller that declares it can apply that exact list field. If the seller doesn't, the launch stops with a validation error naming the package, the list field and the seller, and nothing is sent, so a buy can no longer run with a list the seller was never obliged to apply. To proceed, remove the reference from the package or choose products whose seller declares that field. A collection-list reference also needs a product that allows collection targeting. Lists you manage on your advertiser and campaigns are unchanged by this release.
* Inventory source diagnostics now list what is still waiting at each source, and on whom. `sourceHealth.operations` on `GET /api/v2/storefront/inventory-sources/{sourceId}/diagnostics` was always `null`; it now lists every open media buy, update, creative delivery, manual work item, and account request at that source. Each entry says who it is waiting on (`seller`, `operator`, `buyer`, or `scope3`), since when, and the exact thing being waited for, such as your approval, the task id the sales agent returned, or the creative still to be delivered. An entry is listed for as long as it is open, from its first minute, rather than only after a time limit runs out. When you share diagnostics with an Agent's operator through a debug grant, they see that an operation is waiting on you but not what you have to decide. Nothing to change on your side.
## 5.928.0 — October 6, 2026 at 2:31 PM UTC
* A buyer agent can now read the campaigns of an advertiser another organization delegated to yours, when that delegation includes `campaign.read` and the agent holds a `READ` grant on the advertiser. Pass the advertiser's ID to `list_campaigns`, or read one of its campaigns with `get_campaign`. Each campaign read this way carries `delegatedBy` with the owning organization's name. `get_campaign_delivery` returns the delivery Apostra last stored for that campaign, marked `authority: "platform_mirror"`, rather than calling the seller live. The agent's session stays on your account, and nothing on the delegated advertiser can be changed. A delegation with `advertiser.read` only is still refused, and the error now names the missing `campaign.read`. Limited beta: available to organizations with the advertiser delegation entitlement.
* Choose an IU plan now uses the same design as Plan & billing. The plan cards, the review-and-authorize step, and the renewal controls are redrawn on our current design system, the accept and Done buttons sit in a consistent bar at the bottom of the panel, and the loading and error states are drawn inside the panel instead of appearing as loose text.
* Nothing about the plans themselves changed: the same prices, terms, rollover, and renewal rules, confirmed the same way, against the same offer version you were shown.
## 5.927.1 — October 6, 2026 at 2:02 PM UTC
* Fixed Scope3-funded voice drafts failing on short radio spots. A correctly read one-line script, such as a six-second read for a 15- or 30-second slot, is now accepted. Generated audio is checked against how long your own script takes to speak, so near-empty or undecodable audio is still rejected. A failed voice leaf now names a short reason, such as `generated_audio_too_short`, in the generation result.
## 5.927.0 — October 6, 2026 at 1:24 PM UTC
* On the v2 Buyer MCP, opening the Campaigns Page now also returns an "Open in the browser" link in the text result, and as `openInBrowserUrl` in the structured result, whenever the Page has an advertiser: the one you name (once the server confirms you can read it), the launched campaign's advertiser, or the advertiser your connection selected. Hosts that cannot show the Campaigns widget, such as Claude Code, Slack agents or a plain MCP client, can use that link to open the same Page in Apostra chat, focused on the campaign you named. A launch with none of these, or from a seller account, carries no link. Nothing to change on your side.
## 5.926.0 — October 6, 2026 at 1:10 PM UTC
* Buyers can reopen the Variant gallery for an existing Creative Session after search or get, including advertiser Library sessions and sessions whose text response was truncated.
## 5.925.0 — October 6, 2026 at 12:44 PM UTC
* Sellers can now edit their listing's website, support, privacy policy and terms of service links from the media kit, under Your listing, What you do, Edit. These links appear on your ChatGPT app listing and your other channel listings.
* Confluence library syncs now accept documented response shapes, so sellers can import pages and supported attachments from affected spaces.
## 5.924.0 — October 6, 2026 at 12:13 PM UTC
* Sellers can now discover Apostra's request-signing keys from our agent URL, as AdCP describes. `https://api.apostra.com` (and `https://api.interchange.io`) answers `get_adcp_capabilities` without credentials, over MCP and as a plain JSON `GET`. The response's `identity` block names our `brand.json` and the origin of our signing keys, so a verifier that follows AdCP's key discovery no longer fails with `request_signature_jwks_unavailable` or `request_signature_brand_json_url_missing`. The authentication docs now describe this path, and note that verifiers should resolve our key from our agent URL, not from the account's operator.
* Keep your selected seller account available when a v3 MCP client reconnects, so you can continue using seller tools after switching accounts.
* Microsoft 365 Library sync now imports Word documents as readable PDF Material revisions.
* Seller agents can now set the listing's subtitle, website, support, privacy policy and terms of service links through `save_seller`'s `listing` field, and read them back with `get` on the Seller with `include: ["listing"]`. These are the values the ChatGPT app listing and its submission package use.
## 5.923.0 — October 6, 2026 at 11:04 AM UTC
* V3 media buys now show a change that is not live yet. `get(kind: "media_buy")` returns `pendingChange` whenever a buy has a submitted change that has not reached the delivering buy: its status, where it is waiting, and how the end date, budget, and creatives will differ. Every other field still shows what is live. `save_media_buy` returns `action: "pending_approval"` instead of `updated` when the buy it reads back still has such a change, so your agent no longer tells a buyer a change is live before it is.
## 5.922.3 — October 6, 2026 at 10:34 AM UTC
* Murph now sends requests for radio spots, voiceovers, and other new audio creatives to Creative Engines generation instead of file upload. Eligible preview buyers without an ElevenLabs or AudioStack connection can leave the engine unset to use Scope3-funded ElevenLabs.
## 5.922.2 — October 6, 2026 at 9:55 AM UTC
* Storefront media buys now send creatives to the exact sales source selected for each product before the buy is submitted. Buys stop with a clear error when that source is missing, inactive, or ambiguous, instead of waiting indefinitely for creative delivery.
## 5.922.0 — October 6, 2026 at 9:30 AM UTC
* If you operate a Sales Agent, its Agent page now shows how many of its products declare AdCP channels, as an advisory note under the **Catalog Product Quality** certification requirement (for example, "Advisory: 9 of 12 products declare AdCP channels"). The count matches the **Product channels declared** readiness warning and covers up to 50 of the active third-party Sources you can see for the Agent (the page says when there are more). It never changes a certification result. Declaring `channels` on every Product, or `applies_to_channels` on each format option, is how you raise it.
* `request_proposals` results stay readable once sellers answer. A buyer agent whose MCP client checks responses against the tool's declared output schema could get a schema error instead of the per-seller outcomes as soon as a seller's answer was recorded, so it couldn't see quotes, products or why a seller didn't respond. Each seller outcome now contains only the fields the tool declares.
* An Agent's required capability declarations now count the creative kinds that products from a managed sales agent accept, and they still load when a product reports no format options. Media-buy targeting checks now see the targeting a managed sales-agent product declares in `overlay_support`.
* When a media buy sets up a new account with a seller, the brand reference now carries a `brand_id` only when the advertiser's brand declares one (in the saved brand reference or the brand's `brand.json`). Earlier account setups could add a generated `brand_id` the brand never published, and sellers opened a separate account for it. Later product discovery for the same brand and operator could then fail with `ACCOUNT_AMBIGUOUS`.
* An account that a seller already holds under a generated `brand_id` keeps its identity:
* If Interchange can find that account, it updates it by its `account_id`.
* If it can't look the account up, it sends the generated `brand_id` only when the seller's acceptance of that identifier for the same account (same operator, same sandbox setting) is on record, so live buys stay on their account.
* Nothing to change on your side. A seller holding a duplicate account can remove it once no live buy depends on it.
## 5.921.1 — October 6, 2026 at 8:49 AM UTC
* Embedded sales agent 1.57.1: refuse GAM targeting the line item would silently drop, stop FreeWheel booking certification blocking go-live until publishers can run it.
## 5.921.0 — October 6, 2026 at 8:28 AM UTC
* The Library's sections are now shown as tabs.
* Org admins of any organization, buyer or seller, can now save a card as the organization's payment method in Plan & Billing → Payment & invoices. Saving a card charges nothing, and it doesn't accept an IU Rate Card, select a plan or turn on paid entitlements. Eligible Apostra platform and IU charges are collected from the card only when the organization has an accepted IU Rate Card and its IU charging is enabled. Invoicing on payment terms is still available, media billing is unchanged, and no card processing fees are added. Only org admins can add, replace or remove the card; other members don't see the option. Nothing to change on your side unless you want to pay by card.
* Recheck payment-terms-held seller-account setup through the buyer's current billing organization within an hour, so a buyer reparented under an organization that has already chosen terms can continue setup automatically.
* Healthy external inventory sources remain discoverable when their public product catalog can serve buyers, even if another source has an ad-server setup or wholesale-pricing problem.
* Restore the verified `audio_30s` selection on the affected Draft Digital to Talpa media-buy route. The repair checks the exact source, route, package and archived product version before changing the historical snapshot; every other buy is left unchanged.
* When you launch a media buy, every package now tells the seller how to pace its budget. Packages had only carried pacing when you set it on the individual product line, so a campaign set to `EVEN` sent no pacing at all, and a seller could fall back to spending as fast as possible.
* Each package now uses the pacing you set on its product line. Without one, it uses the campaign's `budget.pacing`. If neither is set, it uses `even`, AdCP's default. The pacing you read back on a package is the pacing its seller applies.
* Meta does not accept a pacing field yet. An `even` pacing is left off Meta requests, because AdCP treats a missing pacing as `even` and Meta's standard delivery already spreads spend evenly. A Meta buy on an `ASAP` or `FRONTLOADED` campaign still launches on that even delivery, and the launch response carries a `pacing_not_applied` warning naming the pacing you asked for and what Meta applies instead.
* Changing a campaign's pacing does not change media buys that are already live. Update the package's pacing directly instead. You do not need to change anything else.
## 5.920.0 — October 6, 2026 at 7:38 AM UTC
* Proposal requests now send a campaign's saved demographic targeting to storefronts on the Storefront-built and Agent-supplied paths as the AdCP 3.2 `targeting_overlay.demographics` field, so those storefronts apply the campaign's exact age range. Built-in platform adapters and external sellers still receive the brief only.
* Embedded sales agent 1.57.0: add a read-only GAM line item cap audit ops script, refresh each live package's GAM readback on every scheduled read and flag spend-unsafe drift.
## 5.919.0 — October 6, 2026 at 6:07 AM UTC
* When a seller's sales agent pauses a creative for more input (the AdCP `input-required` status) instead of accepting or rejecting it, Apostra no longer reports that the agent rejected the creative.
* **Sellers:** the creative's delivery now fails as `delivery_failed` rather than `source_rejected`, so it no longer reads as a rejection the buyer must replace. If the agent pauses the creative after first accepting it, the delivery fails as soon as Apostra sees the pause, instead of waiting up to seven days. A sales agent that answers a buyer's creative sync with `submitted`, `working` or `input-required` is no longer marked unhealthy in source diagnostics.
* **Buyers:** on a media buy, that creative's sales-agent entry shows the delivery as failed with no rejection reason, instead of rejected by the sales agent.
* Nothing to change on your side.
* A media buy on inventory trafficked in Google Ad Manager through a managed sales agent now goes `active` only after Google Ad Manager confirms its line items hold the booked spend cap, pacing, price, currency and flight dates. Until then it reads `pending_start`. If a new buy or a resume leaves the line items without the booked budget cap or pacing, Apostra pauses them: a new buy stays `pending_start`, and a resumed buy stays `paused` with the resume failing as `AD_SERVER_UNCONFIRMED`. If a live buy's line items are later changed in Google Ad Manager so they can spend past the booking, Apostra pauses them and alerts its operations team. If Google Ad Manager holds other different settings, Apostra's operations team is alerted and works with the seller to correct them. Buys on other sources are unchanged.
## 5.918.1 — October 6, 2026 at 5:37 AM UTC
* `list_buyer_storefront_accounts`, and `GET /api/v2/buyer/storefront-accounts`, now also show whether the seller is on for an advertiser: the one you name with the new optional `advertiserId`, or the one your request selects. The answer is in `ext.interchange.advertiser_activation`, so an active account grant no longer looks as if the seller is in use for that advertiser. Product discovery for an advertiser now also says which sellers it left out because they are not on for that advertiser: `get_products` lists each one in `ext.interchange.storefronts_not_searched` with its reason, including sellers that cannot transact with you at all, gives the fix once, and names them in its text summary. Before, it only counted them. Two product-discovery skip reasons are also corrected. A seller that ran out of time now always reports `failed` with reason `timeout`, never `skipped`. A seller skipped for an eligibility reason now reports `eligibility` instead of `coverage`. Nothing to change on your side.
## 5.917.0 — October 6, 2026 at 4:35 AM UTC
* If your organization has accepted an advertiser delegation from another organization, you can now give a buyer agent read-only access to that advertiser. Nothing changes until an administrator grants it through the buyer-agent access API; the console's advertiser picker does not list delegated advertisers yet. Grant `READ`; `READ_WRITE` is refused. The advertiser then appears in the agent's advertiser lists, tagged with the owning organization's name. The agent's session stays on your own account, and the owning organization's accounts are never listed or selectable. When the delegation ends, the advertiser drops out of the agent's lists and every request for it is refused. Limited beta: available to organizations with the advertiser delegation entitlement.
## 5.916.0 — October 6, 2026 at 4:05 AM UTC
* You can now read the health of an inventory source from one `sourceHealth` object in the diagnostics response (`GET /api/v2/storefront/inventory-sources/{sourceId}/diagnostics`). It names the Agent that powers the source and whether buyer requests reach it. Each AdCP operation Apostra performs against the source, such as `get_products` or `create_media_buy`, gets a status of `healthy`, `degraded` or `erroring`, or `null` when nothing has been observed. It also lists each problem found with who has to act (you, the operator of the Agent behind the source, or Apostra), how serious it is, and the next step. The inventory source diagnostics page in the docs lists every field.
* No action is needed now. `sourceHealth` is additive, and the `source.health`, `source.capabilityHealth`, `diagnosis.severity`, `diagnosis.owner` and `diagnosis.issues` fields are deprecated in its favour. They are still returned unchanged. Move to `sourceHealth` when you next update your integration.
## 5.915.2 — October 6, 2026 at 3:41 AM UTC
* A media buy whose seller confirms it as already paused or completed on its very first response now gets its internal activation timestamp recorded, the same as a buy that confirms active. Previously this timestamp was only recorded for the active case, which could cause unrelated reconciliation checks (including `update_media_buy`'s failure reporting) to behave incorrectly for a buy created this way.
## 5.915.0 — October 6, 2026 at 2:58 AM UTC
* A pause that a seller accepts but has not applied yet is no longer shown as paused. When a seller answers with "accepted, still processing", pausing a media buy now returns `202 Accepted` with `pauseRequestedAt`, and the buy stays `ACTIVE` (it may still be delivering) until the seller reports it paused; `GET /media-buys/:mediaBuyId` shows `pauseRequestedAt` until then. A campaign pause waiting on such a seller returns `503` and keeps the campaign `ACTIVE`, then shows it `PAUSED` on its own once every media buy confirms. Apostra's operations team is alerted if a seller has not confirmed within two hours. Resuming works the same way: a resume the seller is still applying returns `202` with `resumeRequestedAt`, and the buy stays `PAUSED` until it is delivering again. A pause or resume confirmed straight away still returns `200` as before. On the V3 preview, `save_media_buy` with `isPaused` reports these as the new actions `pause_requested` and `resume_requested`, keeping the buy's live status and adding a `pendingChange` with the status it moves to.
## 5.913.2 — October 6, 2026 at 1:38 AM UTC
* Pausing a media buy on an ad-platform storefront now works even while an earlier change to the same buy is still pending, for example a budget change waiting for the seller's approval. This includes an agent calling the storefront's AdCP `update_media_buy` with `paused: true` directly, which was previously refused with `CONFLICT` (`mutation_in_flight`). The earlier change keeps its place, and when it is applied the buy stays paused. If a seller or ad platform refuses or fails a pause, you still get an explicit error, and Apostra's operations team is now alerted. Nothing to change on your side.
## 5.913.1 — October 6, 2026 at 1:04 AM UTC
* `update_media_buy` no longer reports a sync failure to a buyer for a confirmed seller change on a media buy whose record is missing an internal activation timestamp. The seller's own confirmed response is now trusted for that one confirmed-safe case; every other mismatch still fails exactly as before.
* If you turn a media partner off for an advertiser after searching, a saved search for that advertiser no longer shows that partner's products, and adding one of them to a buy, directly or by applying a proposal, is refused right away with a message saying where to turn the partner back on. Previously the products stayed visible and the buy was only refused when you created it. Media-buy creation now gives the same message, including when a partner is excluded for your whole account. Nothing to change on your side.
## 5.913.0 — October 6, 2026 at 12:19 AM UTC
* Document Apostra as the default assistant identifier, preserve the Murph migration path, and add production adoption views for retained identifiers.
* Add canonical Apostra MCP endpoints, tool discovery, and UI resource aliases while keeping the existing Murph MCP endpoints available without redirects.
* Add Apostra REST API paths alongside the existing Murph paths, with equivalent authentication, authorization, rate limiting, conversation, and error behavior.
* Add `/api/v2/apostra` as the canonical REST path family for the assistant while keeping existing `/api/v2/murph` clients fully compatible.
* Storefronts on the Storefront-built and Agent-supplied paths no longer refuse a product search because a number in the brief looks like an age. A budget or duration such as "Budget 2500 EUR over 20 dagen", or a description such as "our customer is generally in her 30s", now goes through to discovery without restricting delivery. Hard age targeting now belongs in structured fields: these storefronts accept the AdCP 3.2 `targeting_overlay.demographics.age` field, and a brief that requires or excludes an age range, such as "target ages 25-54", is refused with `INVALID_REQUEST` and an instruction to send the range in that field or to say in the brief that it is a preference, not a hard requirement. This is read however the brief is worded, including in words or another language. If the brief cannot be read at that moment, the search returns a retryable error. Built-in platform adapters such as Meta and TikTok are unchanged.
* Third-party sales-agent storefronts can read and update their durable certification sandbox-account nomination reliably. When the read fails, the inventory source now shows the underlying error code.
* Your ChatGPT app download now works with OpenAI's plugin submission flow. It contains one plugin ZIP you upload to OpenAI unchanged. That ZIP pre-fills your listing, starter prompts, review test cases, and release notes, and for buyer apps it includes the current skills. Every download carries a higher version, so you can upload a fresh one to the same plugin in OpenAI as an update. OpenAI limits plugin names to 30 characters, so a storefront name longer than that now blocks the package until you shorten it.
* Agent and machine-to-machine (M2M) tokens issued for `https://api.interchange.io/mcp` or `https://api.apostra.com/mcp` are now accepted on both hosts. Existing integrations keep working as Apostra moves its default API host. No action is needed.
## 5.912.0 — October 5, 2026 at 11:42 PM UTC
* If your integration reactivates campaigns, handle an error from the reactivate call: a campaign reactivate now returns `503` with each media buy's outcome when any media buy fails to resume. This applies to `POST /api/v2/buyer/campaigns/:id/reactivate` and to setting `isPaused: false` on a campaign. Before, the call returned success even when some media buys stayed paused. Media buys that resumed stay active, and the error lists the ones still paused, with the seller's reason where one was given; resume those one by one with reactivate media buy. A campaign now always shows `ACTIVE` while any of its media buys is delivering and `PAUSED` only when all of them are paused. Campaigns that were left showing `PAUSED` while a media buy kept delivering are corrected to `ACTIVE`; no media buy is paused or resumed by this correction.
## 5.911.0 — October 5, 2026 at 11:15 PM UTC
* Product discovery now lists the targeting a seller declares under **Targeting buyers can set** in a product's `supported_targeting`, using AdCP dimension names such as `geo_countries` and `frequency_cap`. Before, a product that declared targeting this way showed no supported targeting to buyers.
* Embedded sales agent 1.56.0: apply update\_media\_buy pacing changes to GAM and confirm them, cap every AdsWizz campaign at its booked budget, pace it as the buyer booked and read it back, paid Broadstreet buys now need the seller's Broadstreet account in manual mode; automatic and confirmation-required Broadstreet buys are refused before anything is booked, because ESA cannot set a budget cap in Broadstreet, refuse creative-side writes to a GAM line item that has no goal to stop it, refuse FreeWheel booking terms that would not stop delivery at the booked budget.
## 5.910.0 — October 5, 2026 at 10:39 PM UTC
* Third-party sales-agent sources now retain their recorded certification sandbox-account selection when the source is disabled.
## 5.909.0 — October 5, 2026 at 10:16 PM UTC
* Slack now presents Apostra consistently in app setup and Product Update copy. The canonical `/apostra` command is promoted even when a saved `/murph` shortcut is used; `/murph` remains available during the documented migration.
## 5.908.0 — October 5, 2026 at 8:57 PM UTC
* Rolling out account by account: when you create a media buy whose optimization goal names one of your event sources, Apostra now binds that source to an existing pixel (a Meta dataset or Snap Pixel) on each seller account the buy runs on. The pixel ID is not yet included on the media buy sent to the seller, so sellers do not optimize against it yet. Read each account's `sellerId` (the pixel ID), `status` and `blocker` under `sellerAccounts` on the event source. Binding needs the **Conversion events** switch on for that seller in Connections; otherwise the blocker is `SELLER_DATA_SHARING_NOT_PERMITTED` and nothing is sent. Apostra never creates a pixel. If an account has several and none is named, the blocker is `SELLER_EVENT_SOURCE_DESTINATION_AMBIGUOUS`; name one with `sellerAccounts` in `save_event_source`. If it has none, the blocker is `SELLER_EVENT_SOURCE_DESTINATION_MISSING`. The media buy never waits for binding.
* Source diagnostics now show transport evidence for ad server sources. Each outbound request Apostra sends to the sales agent behind your ad server source, such as a media-buy create, now appears in `transportRequests` with the same redaction as a third-party sales agent. Before this change, those requests were missing from that list. For these rows `destinationOrigin` is `null`, because the request goes to a service Apostra runs internally.
* Source diagnostics in Murph now uses the same card, header, buttons, status labels and form fields as your other storefront pages. What it shows and what you can do there is unchanged: you still see whether each inventory source's connection is working and the one next step, open Technical details for the endpoint, authentication and request-signing evidence, check the connection, update credentials, run discovery and validation tests, and connect a new sales agent. Nothing to change on your side.
* Reading a campaign with V3 `get` now returns its `optimizationGoals`, primary goal first, in the same shape `save_campaign` takes, so you can read a campaign's goals and send them back unchanged. A cost or return target still reads as `bidding`, not on a goal. Reading an event source now also returns `health` on each `sellerAccounts[]` entry: the seller's own assessment of the confirmed pixel on that account, in the AdCP `EventSourceHealth` shape. A pixel that is set up but has not received events yet shows `events_received_24h: 0` with no `last_event_at`, so you can see it needs events before you rely on it for optimization. It is `null` until a pixel is confirmed or when the seller gives no assessment.
## 5.907.0 — October 5, 2026 at 8:04 PM UTC
* Embedded sales agent 1.55.0: paid Google Ad Manager line items booked through an embedded sales agent are now capped at their booked budget and paced the way the buyer booked them, with even pacing when the buyer asks for none, so a buy can no longer deliver past its budget. After every create, update, and pause, the embedded sales agent reads the line item back from Google Ad Manager and reports the settings Google Ad Manager actually applied. Products whose Google Ad Manager spend settings an older version filled in automatically are corrected when this version deploys, so they can be booked again. Spend settings a seller wrote are left unchanged; a product with no spend cap configured cannot be booked until the seller sets one. When a product's own delivery setting books a package slower than the pacing the buyer asked for, the create response now includes a warning that says so. Nothing to change on your side.
* Return a retryable reporting-store failure and release every durable modular booking during a media-buy cancellation.
## 5.906.0 — October 5, 2026 at 7:24 PM UTC
* The advertiser form now states the scope an empty list actually means. Leaving channels blank reads as **All channels**, the same way an empty country list already reads as **All countries**, and you narrow either one from the same picker. The old "Select at least one channel to manage sellers" note is gone — it described a rule the platform no longer applies.
* An advertiser created before country scope was recorded can no longer be saved with the question unanswered. Editing it asks you to name its countries or confirm it works in all countries before Save is enabled, so the advertiser reaches sellers on a scope you chose rather than on a blank.
* Nothing changed about what the API accepts: an empty array is still valid and still means "all".
* If your integration pauses campaigns, handle an error from the pause call: a campaign pause now returns `503` with each media buy's outcome when any media buy fails to pause, and the campaign stays un-paused. This applies to `POST /api/v2/buyer/campaigns/:id/pause` and to setting `isPaused` on a campaign. Before, the call returned success and showed the campaign as `PAUSED` even when some of its media buys were still delivering. Media buys that did pause stay paused, and the error lists the ones that may still be delivering, with the seller's reason where one was given. Retrying is safe, because a pause only targets media buys that are still active. When every media buy pauses, nothing changes: you get `200` and the campaign is `PAUSED`.
* On storefronts that forward to a sales agent, resuming a paused media buy on its own no longer waits for seller approval: a resume with no other change restores terms the seller already approved. A resume that comes with any other change, such as a budget, date, or package edit, still goes to the seller for approval. On ad-platform storefronts with manual review, every resume still waits for the seller. The buyer media-buy and reactivate pages and the seller approvals guide now describe this. Nothing to change on your side.
* Products the Merchandising Agent builds can now require signals together, not only as alternatives. A product for decision makers at small and mid-sized businesses targets people who are decision makers AND work at a company of either size, instead of anyone in any one of those segments. The agent adds a requirement only when the brief calls for it, since each one narrows reach, and the seller's ad server receives that structure unchanged. A product that requires signals together can only be bought with AdCP 3.2; an AdCP 3.1 media buy for it is refused with an error that says so, rather than sent as one broader either-or audience. The agent also no longer offers a product that lists a seller's Google Ad Manager key-value signal and an audience segment as alternatives, which Google Ad Manager cannot deliver, when the seller's ad server connection identifies which kind each signal is. Products built before this release are unchanged.
* When you search for products for an advertiser, only the media partners turned on for that advertiser are searched, and partners that are off are no longer counted as failed sellers. If you name a media partner that is off for the advertiser, its result now says it wasn't searched and where to turn it on: open Media Partners for that advertiser, open the media partner, choose Controls, then set Availability to On. A partner excluded for your whole account is changed in the account view instead. If no media partner is on for the advertiser, the search says that instead of reporting that no connections are configured. Nothing to change on your side.
* People without a Gravatar account now show a coloured initials tile instead of a generated geometric pattern. Team rosters, user management, and activity feeds read as names at a glance rather than as abstract shapes, and anyone who does have a Gravatar still sees their own photo.
* In Media Partners, an advertiser's status at a seller whose agent lists its accounts now follows that seller's later decisions. An account the seller is still reviewing shows **Waiting on media partner** until the seller approves it, then shows **Active**. An account the seller later suspends, closes, or rejects stops showing **Active**. Apostra re-checks every few hours, and every 15 minutes while an account is under review. If two of a seller's connected sources list the same account with different statuses, that account keeps its last status until they agree. Nothing to change on your side. The seller still decides whether to accept a buy on an account it has suspended or closed.
* Embedded sales agent 1.54.1: re-include excluded ad units by their history, not only a 7-day timer, re-judge a package's ad unit exclusions when its goal changes, read back booked GAM line items and record drift per package, record ad unit attribution coverage on every placement optimization pass, return GAM-confirmed settings after create, update and pause, cap every paid GAM line item at its booked budget and pace it as the buyer booked, report a day still without data past the worst-case lag as nothing delivered, stamp the tenant status cache entry when it is stored, store the booked package budget and test generated GAM defaults through the buy lifecycle, stop the GAM write canary blocking go-live until publishers can run it.
* The Discord setup guide now explains how an existing installation can be renamed to Apostra while keeping its connection and access. Your administrator must complete the provider change.
## 5.904.0 — October 5, 2026 at 5:45 PM UTC
* Buyers can keep discovering products from healthy inventory sources when another source has an ad-server setup or wholesale-pricing problem. Products from the affected source stay unavailable until the seller fixes that source; existing campaigns do not need to be recreated.
## 5.903.1 — October 5, 2026 at 4:54 PM UTC
* Pausing a media buy now stops delivery straight away, even when the seller's storefront reviews every change manually, and on guaranteed buys too. A pause no longer waits in the seller's approval queue while the ad server keeps serving. The pause response now says whether the seller's storefront confirmed it: a `200` means delivery stopped, and an error means it was not confirmed and the buy may still be delivering. A pause never returns a 409 because it is waiting for approval. Sellers get a new `storefront.media_buy_buyer_paused` notification when a buyer pause takes effect on their inventory. It asks for no action. A pause does not change the buy's budget, flight dates, or terms. Resuming a paused buy still follows the storefront's normal approval rules. Nothing to change on your side.
## 5.903.0 — October 5, 2026 at 4:01 PM UTC
* Managed sales-agent sources now use the certification buyer's admitted sandbox storefront account for live-traffic certification. Sellers name a no-charge test advertiser in their ad server through the normal Buyer Account Mapping decision, using an active dedicated binding; they do not choose a separate certification sandbox account for those sources.
## 5.902.0 — October 5, 2026 at 3:34 PM UTC
* Agent text lines can now send RCS with `POST /api/v2/communications/rcs/messages`. It takes the same request ID, line, recipient and text as `POST /api/v2/communications/messages`, which still sends SMS only and is unchanged. RCS shows your verified business name and logo in RCS messaging apps such as Google Messages. It only works after Apostra binds a verified RCS agent to your line; until then the command returns `400` and SMS keeps working. RCS uses the same queue, opt-out record and sending limits as SMS. A `202` response means the message is queued, not delivered, and RCS never falls back to SMS.
* Restore Confluence source previews by respecting Confluence's per-request page limit while continuing to preview up to 500 items.
## 5.899.0 — October 5, 2026 at 2:10 PM UTC
* When you list campaigns without a `status` filter, the response now tells you how many campaigns the default filter (active, draft, paused) left out, broken down by status, and that `status=ALL` includes them. An empty campaign list for an advertiser whose summary shows campaigns now explains the difference: those campaigns are completed or canceled. Nothing to change on your side; lists that send `status` are unchanged.
* Buyer agents now get access to advertisers that another organization has delegated to their organization. These agents include hosted agents, API-key agents, and WorkOS machine-client agents. Before this fix, the check looked up the wrong organization, so an accepted delegation never applied to the agent.
* Buyer agents connected to the Buyer MCP server no longer get an account switcher they cannot use. Their tool list leaves out `customer_switch`, and `list_accounts` now returns the account the agent is working in (marked `current: true`) plus each account that owns an advertiser you granted it, instead of every account of the person the agent acts for. Buyer agents still cannot switch accounts, and grants work as before. People and API keys keep `customer_switch` and the same account list. Nothing to change on your side.
* Failed media buys now explain when a selected product needs seller correction or when buyer targeting is invalid. The response keeps the specific correction and identifies who should act instead of reporting a platform error. Retrying the same structurally invalid buy is not recommended.
* Seller approvals received after an earlier media buy fails now carry over to later buys that use the same unchanged creative. Edited creative content still requires a new approval, and buyers do not need to take action.
## 5.898.1 — October 5, 2026 at 1:41 PM UTC
* When Meta refuses an audience that a data partner such as LiveRamp shared into your ad account, `create_media_buy` now returns `PERMISSION_DENIED` with reason `partner_audience_sharing_required` and tells you to remove or replace that audience. Previously it returned `SERVICE_UNAVAILABLE`, which looked like a temporary outage, and the media buy was retried automatically even though Meta refuses it every time.
* When Meta refuses an audience change in `update_media_buy` because a data partner such as LiveRamp shared that audience into your ad account, the update now returns `PERMISSION_DENIED` with reason `partner_audience_sharing_required`, says no changes from the update were kept, and tells you to remove or replace that audience. Previously it returned `SERVICE_UNAVAILABLE`, which looked like a temporary outage. If we can't confirm that every change from the update was undone, you still get the existing "do not retry, reconcile first" error.
## 5.898.0 — October 5, 2026 at 1:20 PM UTC
* You can now dictate messages to the assistant in the Apostra web app instead of typing them (the assistant appears as Murph on accounts not yet switched to Apostra branding). Select the microphone in the message box, speak, then select it again to stop. A recording also stops on its own after two minutes and is transcribed. Your words appear at your cursor so you can check or edit them. Nothing goes to the assistant until you send the message. The microphone appears only in browsers that can record audio, and your browser asks for microphone permission the first time. The recording is sent to Apostra and transcribed by Google Cloud Speech-to-Text in the United States, in the language you chat in; Apostra does not store it. If the recording has no speech in it, nothing is added to your message. To help spell names correctly, the transcription request includes your account's own advertiser, campaign and storefront names, and names are never shared across accounts. Dictation is unavailable once you reach your daily usage limit, until it resets. Nothing to set up on your side.
* Choose payment terms before Apostra opens a seller account for a consolidated-billing purchase. Until then, discovery uses public seller products when available instead of account-specific pricing. Saving terms immediately restarts held setup for the billing organization and its current direct children; direct-billed and sandbox purchases are unchanged.
* In Media Partners, when a seller that sets up accounts automatically declines an advertiser's account, the advertiser no longer shows **Active**. If the seller rejects, suspends, or closes the account, the advertiser shows **Failed**. If the seller needs payment, it shows **Action required**, and you settle that with the seller. Accounts the seller reports active, or still under its review, show as before.
* Verified Buyer signup can create a Buyer account on an eligible archived Seller domain without waiting for manual account preparation. Existing Seller access and storefront history remain intact; domains with active Seller ownership or commercial activity still require review.
## 5.896.0 — October 5, 2026 at 12:01 PM UTC
* Reading campaigns with V3 `get` or `search` now returns each campaign's `bidding` policy, the cost or return target on the primary goal, in the same shape `save_campaign` takes. A target set through V2 reads back the same way, with a missing strength shown as `cap` for a cost target and `target` for a return target. In V2, each row of `GET /api/v2/buyer/campaigns` now includes the campaign's `performanceConfig` (its optimization goals and their targets) when it has goals, so you no longer need a second read to see a campaign's goals.
* Ask your agent to open your dashboard and Seller Dashboard now opens. Before this, an agent on the v3 MCP endpoint, including Murph, had no way to open it and could open a different Page instead. While your account has an active Demo Storefront, the Dashboard opens on the Demo Storefront evaluation view of your practice runs, and your Demo still reads "Paused before going live". Every other seller sees seller analytics.
* Agents and SDK clients use the new `open_seller_dashboard` operation. It takes no arguments and is available only to Seller Accounts. Nothing to change on your side.
## 5.895.0 — October 5, 2026 at 11:43 AM UTC
* If your inventory comes from a managed sales agent, you can now choose an active sandbox account for live-traffic certification. Nothing changes for other sources.
## 5.894.2 — October 5, 2026 at 11:05 AM UTC
* Hosted ElevenLabs previews now send quoted voiceover copy, retain playable audio MIME types, and reject implausibly short generated audio before it is shown as complete.
## 5.894.0 — October 5, 2026 at 10:20 AM UTC
* Seller source managers, including staff acting in the seller account, can choose or clear the certification sandbox account with clearer inventory-source API and agent wording.
## 5.893.2 — October 5, 2026 at 10:00 AM UTC
* Media buys that use a seller's ad-server signal now reach the seller when that signal omits its optional inclusion mode. Explicit exclusions and invalid signal settings still behave as before. Nothing to change on your side.
* Approved creatives created before format-option pins were stored can now use the media buy's exact package selection when being delivered to a legacy sales agent. This restores delivery without recreating the creative or media buy.
## 5.893.1 — October 5, 2026 at 8:54 AM UTC
* New Apostra documentation, generated API contracts, SDK clients, and agent setup instructions now use `api.apostra.com` by default. Existing `api.interchange.io` integrations keep working and need no changes.
## 5.892.0 — October 5, 2026 at 2:52 AM UTC
* In the Webhooks Page, you can register a public HTTPS endpoint, prove that you control it, and rotate or disable its signing key. Copy each new signing secret when the Page shows it: it is not shown again. Event subscriptions remain a separate setup step.
## 5.890.0 — October 5, 2026 at 1:24 AM UTC
* A Demo Storefront's seller rail shows **Dashboard** again, under Operate. It opens the Demo Storefront evaluation view of Seller Dashboard, where your practice runs appear as synthetic evaluation data. Opening it does not mark the Demo as live: it still reads "Paused before going live". Storefronts that are not live and are not a Demo still see no Dashboard row. Nothing to change on your side.
* Flag saved Library responses when a cited document changes and let sellers re-run the saved brief.
* If you switch into another account that no longer has Buyer or Seller access, the app now takes you back to your home account and keeps you signed in. Before, it signed you out and showed the sign-up form.
* You can now see your record against the goal commitments you have made on completed buys. Ask for it with `include: ["commitmentRecord"]` on your own seller read (`get` with `kind: "seller"`). For each goal objective and commitment kind (`guaranteed`, `best_effort`, `report_only`), it shows how many completed buys were judged, how many beat, met or missed the commitment, the hit rate, and the median distance from target. It is judged the same way buyers see goal progress on their own buys, and it never names a buyer, a campaign or a media buy.
* Buyers will later be able to read this record when choosing a seller. They will see a figure only when at least 3 distinct buyers and at least 5 judged buys stand behind it, and never who those buyers are. We will tell you before buyer access opens.
* The Set Up a Campaign skill (version 1.7.0) now tells your agent to put a cost or return target in the campaign's `bidding` policy, and to ask how firmly sellers should hold it, instead of putting the target on a goal, which V3 `save_campaign` now refuses.
* V3 `save_campaign` now takes a campaign's cost or return target as an AdCP 3.2 `bidding` policy on the primary goal: `bidding: { cost_per: { amount, strength } }`, where `strength` is `cap` or `target`, or `bidding: { roas: { value, strength } }`, where `strength` is `floor` or `target`. `strength` says how firmly sellers should hold the number, and you must give it. **Breaking change for V3 callers:** a `cost_per` or `per_ad_spend` target on any goal is now refused, and the refusal tells you to use `bidding`. Goal targets such as a minimum rate or "as much as possible" are unchanged. V2 campaign calls are unchanged. A cost target set through V2 on a secondary goal stays in force when a V3 save sends that goal back unchanged; if the save removes or changes that goal, the response names the target it removed.
## 5.889.0 — October 5, 2026 at 12:19 AM UTC
* A card you add on Plan & Billing is now saved as your payment method once it's confirmed. Before this fix, a confirmed card could be marked confirmed without being saved, so Plan & Billing asked you to add a card again.
* The Campaigns view now shows each campaign's and media buy's progress against its goal: what you asked, what the seller committed to, what has been delivered, and whether it is on track, behind, or beat target.
* Plan & Billing no longer offers to add a card when it can't check on a card you already submitted. If the card confirmation status fails to load, the Payment method section now says so and offers a retry, so you don't submit the same card twice. If your session has expired, you're sent back to sign in.
## 5.888.1 — October 4, 2026 at 10:15 PM UTC
* A newly created seller account, including a new Demo Storefront, now shows its status line in the account switcher straight away. Before, the line could be missing for several minutes after the account was created.
## 5.888.0 — October 4, 2026 at 10:02 PM UTC
* The seller rail now reads "Paused before going live" for a Storefront that is paused and has never gone live, including a new Demo Storefront. Before, it read "Setting up" even when every required step was done. Once every final validation step is done, the rail also stops listing that group, so a full count such as "15 of 15" no longer appears beside a step that looks outstanding. Select the count to see the complete checklist.
* Generated image variants now show an automated brief-match result before a buyer chooses or refines an output. A failed check prevents finalising that output, while warnings stay available for human review.
## 5.887.1 — October 4, 2026 at 9:36 PM UTC
* When your Sales Agent returns an `account_id` from `sync_accounts`, Apostra now stores it and sends `account: { account_id }` on later `get_products` and `create_media_buy` calls for that account, instead of the brand and operator. A buyer's discovery or buy now reaches the account you named, even when the same brand and operator match more than one account on your side. Agents that return no `account_id` keep receiving the brand and operator as before. Nothing to change on your side; see Sales Agent requirements for the recommended practice.
## 5.887.0 — October 4, 2026 at 9:22 PM UTC
* When you tell Murph about a problem with the API key form or field, it now answers your question. Before, mentioning the API key form could make Murph reply only with a link to the secure credential form. If you offer to send Murph an actual API key, it still sends you to the secure form and won't take the key in chat.
* Brand references that Interchange sends to a Sales Agent now contain only the fields that the Agent's AdCP version defines: `domain`, `brand_id`, and, from AdCP 3.1, `industries`, `data_subject_contestation` and `brand_kit_override`. This applies to `get_products`, `sync_accounts`, `create_media_buy` and `update_media_buy`, in both `brand` and `account.brand`. The advertiser's brand `countries` stay inside Interchange and are never sent. Some AdCP 3.1 Sales Agents had rejected product discovery when a brand carried `countries`; that request now validates. Nothing to change on your side.
* Sellers behind an Apostra storefront now receive an advertiser's property lists when they support them. For a product sold through a storefront, Apostra checks whether the sales agent behind the storefront declares `property_list` or `property_list_exclude` in its AdCP capabilities. Before, it checked the storefront's own capabilities, which never declare lists, so these sellers never received one. When the sales agent declares support, the include and exclusion lists go with each new buy and with every later list change, and the storefront passes them to the agent unchanged. Sellers that don't declare support, and managed sales agents, receive no list, as before.
## 5.884.0 — October 4, 2026 at 4:36 PM UTC
* Webhooks you sign with RFC 9421 message signatures now resolve your signing keys from your agent's `get_adcp_capabilities`. We use the `identity.brand_json_url` your capabilities declare. If you are an AdCP 3.x agent that declares none, we fall back to `/.well-known/brand.json` on your agent's origin, so your current setup keeps working. Your agent must answer `get_adcp_capabilities` without credentials, over the protocol you registered with us, for a signed delivery to verify. If we cannot fetch your capabilities, `brand.json` or JWKS, we respond `503` so you can redeliver. HMAC-signed callbacks are unchanged. See [Signing the webhooks you send us](/v2/storefront/inventory-sources/webhook-signing).
* Products the Merchandising Agent builds now say exactly how they use each signal, and that selection reaches the seller's ad server unchanged. For a key whose values are chosen per buy, such as a Google Ad Manager site-section key, the agent picks the values that fit the brief; for a numeric signal it picks a range; audience segments are included or excluded as before. Products that use such a key can now be bought with AdCP 3.2; before, the ad server refused those buys. This does not yet cover a signal the seller set up as one fixed key-value pair, or a signal combining several conditions: the seller's ad server connection still refuses those over AdCP 3.2, and they keep working only over AdCP 3.1 until that connection is updated. A product that narrows a signal to specific values or a range can only be bought with AdCP 3.2: an AdCP 3.1 media buy for it is refused with an error that says so, rather than sent without the values. Products built before this release keep their old signal delivery; one that uses a per-buy key still cannot be bought with AdCP 3.2 and needs to be rebuilt from a new brief.
* Account administrators can now convert a standalone account into an organization without adding a second account. In **Settings → Account configuration**, select **Convert into an organization**. The account becomes the organization's first account: its contract, billing authority, plan (with its feature access and account allowance) and any sales agent offering move to the organization, and you become an organization administrator. The storefront, advertisers, campaigns, history and members stay with the account, and other members stay account-only unless invited to the organization. Converting can't be undone in self-service. Nothing changes for an account unless an administrator converts it.
* Coaching reruns, Run again, and revision turns in Proposal Pass now keep the original request's budget, flight, targeting, and constraints, and a coached practice rerun keeps the original truth rules for unsupported claims and custom fulfilment. Before, the new response was built from the brief text alone, so it could drop the dollar amounts and the buyer's guardrails while looking like an improvement. On a Demo Storefront practice run, coaching now changes the proposal only through the AI model, as it does for every seller; the fixed "Confirm the allocation" next ask added in the previous release no longer replaces it, and the comparison says plainly when a rerun produced no visible change.
## 5.883.0 — October 4, 2026 at 3:10 PM UTC
* Uploading a creative from ChatGPT now completes instead of stopping at storage.
## 5.882.0 — October 4, 2026 at 12:48 PM UTC
* `get_delivery` now limits storefront reporting to the requested media buy. Buyers asking for one media buy no longer receive delivery results from other media buys in the same storefront.
## 5.881.1 — October 4, 2026 at 5:55 AM UTC
* You can now create native in-feed products on a Google Ad Manager source in Inventory Components. When you choose native in-feed (or a sponsored placement, image carousel or responsive format) on Google Ad Manager, the draft form and the product editor ask for the Google Ad Manager creative template ID that renders it, and send it with the product so its line items book against your template. The ID is required and must be a whole number. A 1×1 Google Ad Manager slot now preselects native in-feed. If a product already has one of these formats with no template ID, the editor warns that it can't be booked until you add one. Other ad servers don't need a template ID.
## 5.881.0 — October 4, 2026 at 4:57 AM UTC
* When you edit a product in Inventory Components, you can now choose the targeting buyers may add to a package after they find it, such as countries, regions or audiences. Only the targeting your ad server can apply is offered, and you can set how many countries one package may target. Your prices and floors still apply to every selection you allow, so turn on only targeting you're happy to sell at those prices. Targeting you don't change stays exactly as it is. Nothing to change on your side.
* When you draft a product in Inventory Components, you can now choose the targeting buyers may add to a package after they find it, such as countries, regions or audiences, just as you can when editing a product. Only the targeting your ad server can apply is offered, and you can limit how many countries one package may target. Targeting is optional: leave it off and the product declares none. Your prices and floors still apply to every selection you allow. Nothing to change on your side.
* Editing a property list no longer sends that list to sellers that never said they support it. When an advertiser's or a campaign's include or exclusion list changes, Apostra pushes the new list to every active buy, but only to sellers that declare support for that kind of list, the same rule a new buy already follows. Before, the include list went to every seller, and a seller that doesn't support it could reject the whole update, so the edit failed on that buy. A buy whose seller can't take the change keeps running without it, and the skipped change is logged. Pausing those buys and telling the buyer is a separate change.
## 5.880.0 — October 4, 2026 at 3:29 AM UTC
* Seller Account Admins can nominate the sandbox account used for live-traffic certification from the inventory-source API or v3 inventory source save.
* When a product in Inventory Components has a video format, you can now say where its video ads play: in-stream, accompanying content, interstitial or standalone. Pick as many as apply, both when you draft a product and when you edit one. Buyers see the types you choose. Leave them all unticked if you don't want to say. A stored value that isn't one of these types stays as it is until you change the selection. Nothing to change on your side.
* The Inventory Components draft form no longer offers CPM while it is still finding out which pricing models your ad server accepts. The pricing section shows that it is loading until your source answers, and if the source can't be reached it says so and offers **Try again**, instead of falling back to CPM. CPM-only pricing now appears only when your source answers that it reports no pricing models.
## 5.879.0 — October 4, 2026 at 2:29 AM UTC
* You can now add an API key or bearer token to a sales agent you saved with "No auth", without creating a new source. Open the source's diagnostics, expand **Connection details** and choose **Add credentials**. The same action appears at the top of the page if a connection check fails with an authentication error. Saving switches the source from No auth and stores the secret in one step.
* When an Apostra buyer's campaign sets an explicit strength on its primary cost goal, an AdCP 3.2 seller now receives the cost target in the proposal request at that strength. A goal marked `target` reaches only sellers that accept a media-buy `cost_per` target, rather than always being sent as a `cap`. Cost goals with no stated strength are unchanged and still arrive as a `cap`.
* Proposal Pass now lists a proposal file (JSON, HTML, PDF or PowerPoint) as soon as your request is accepted, even if the page's next refresh fails or comes back before the file appears.
* When you ask Murph to file a support request about your sales agent's credentials, it now shows you the request and asks for approval as normal. Before, its approval prompt could be replaced with a link to the secure credential form, so you could approve without seeing what was being filed. Murph still sends you to the secure form, and won't take the secret in chat, when it asks for a password or token.
* Sellers on AdCP 3.2 that accept a package bidding policy now receive a campaign's cost or return target in AdCP 3.2's shape. When a seller declares the mode and strength in `features.bidding_policy.package.fixed`, each package carries the primary goal's target as `bidding.cost_per` (amount and strength) or `bidding.roas` (value and strength), and the goal itself carries no target. A cost target with no strength is sent as `cap`, so the seller keeps the average cost at or below it. Every other seller receives the target on the goal exactly as before, as does any package that carries a bid price or a cost target on a secondary goal. The amount does not change.
* For buyers in the product-scoring pilot, auto-select now favours sellers that keep their goal commitments. When it ranks products, a seller that has met or beaten the cost or rate targets it committed to on completed buys ranks above an otherwise identical seller that missed them. A seller with too little completed history keeps the place its other results give it. A guarantee counts fully, a best-effort answer counts half, and a report-only answer does not count. Buyers outside the pilot see no change.
## 5.878.0 — October 4, 2026 at 1:30 AM UTC
* If your storefront's sales agent uses a supported partner contract and its connection is current, it can now receive briefs and bookings through a linked storefront source. Storefronts with a revoked, incomplete, or unsupported partner connection remain unavailable until that connection is ready. There is nothing to change on your side.
* When you select a media partner on Media Partners, each of your advertisers is now set up with that partner automatically. The partner's card moves from **Activation deferred** to **Provisioning** and then **Active**, with no support request. Partners on ad platforms where you connect your own account still need you to connect or map that account, and a partner that asks you to accept its billing terms still needs that acceptance; the card shows the action in both cases. You can also turn a partner on or off for a single advertiser on every buyer account. Nothing else to change on your side.
* Advertiser-scoped Creative Sessions can again be found and opened in search, and their generated variant gallery now renders when the session is large.
* Click-through URLs that use `{CREATIVE_ID}`, `{CAMPAIGN_ID}`, `{PACKAGE_ID}` or `{MEDIA_BUY_ID}` now reach your landing page with the real ids filled in. Before this fix, a click passed through our click tracker arrived with those placeholders still in the URL as literal text, so attribution parameters built from them (for example `utm_content={CREATIVE_ID}`) recorded the placeholder instead of the id. `{PACKAGE_ID}` can be left empty, because one creative can be assigned to several packages. The fix applies to creatives synced to a seller from this release on; clicks already recorded keep the values they arrived with.
## 5.877.0 — October 4, 2026 at 12:08 AM UTC
* When you edit a product in Inventory Components, its ad units and placements are now listed with a Remove control, and you add more by searching the inventory loaded on the Components tab. Publisher coverage works the same way: remove a publisher, or add one of your source's coverage options. A product keeps at least one ad unit or placement and at least one publisher. The editor no longer has any JSON fields: channels, formats, pricing, placements and coverage all use guided controls, and what the controls can't show stays as saved. Nothing to change on your side.
* Embedded sales agent 1.53.2: keep products with unknown channels in channel-filtered get\_products, publish the overlay\_support schema once in the tenant-management OpenAPI, reject products the buyer can't see in create\_media\_buy, verify GAM line-item creative associations before update\_media\_buy reports completed, write the approved click-through into GAM HTML5 bundles' clickTag.
## 5.876.0 — October 3, 2026 at 11:10 PM UTC
* Uploading a file from inside the Creative Library now finishes instead of stopping at verification.
* When you edit a product in Inventory Components, each pricing option is now a card with the same fields as a new draft: delivery type, pricing model, fixed price or auction, rate or floor, and currency. You no longer edit JSON. Only the models your source can book are offered, and pricing stays read-only until your source's pricing models have loaded. You can add and remove options, but a product keeps at least one. An option you don't change is saved exactly as it was, and an option in a model the cards can't show, such as CPP, stays as saved unless you remove it. Nothing to change on your side.
* On Media Partners, a partner marked **Activation deferred** now reads "Setup hasn't started yet; no action needed" instead of saying activation is paused. Deferred means automatic setup for that advertiser hasn't run yet, for example while the platform confirms which of the partner's connections to use. Nothing was paused on your side. Partners that need an action from you still show that action instead. Nothing to change on your side.
* Embedded sales agent 1.53.1: classify GAM faults from their documented type and reason, classify GAM flight-date sync failures and keep FreeWheel binding text out of buyer errors, mark acceptance-test traffic in tenant-management logs and Sentry, read GAM video completions from the column GAM returns, keep each GAM tenant's 30-day per-ad-unit delivery history, publish viewable rate and completed views ranges in GAM product forecasts, start GAM goal line items with history's low-performing ad units excluded, store and project AdCP 3.2 overlay\_support on wholesale products, store and return video placement types on wholesale products, apply template visibility to dynamic product variants in get\_products, keep application loggers enabled when Alembic runs in-process, type FreeWheel clients against a request protocol instead of ignoring errors, book GAM template-rendered formats against a real creative template, echo applied signal\_targeting\_groups on update and idempotent create replay, keep deprecated targeting\_capabilities and publish the AdCP overlay\_support schema, report each adapter's own pricing models in inventory capabilities, target GAM audience-segment signals through customTargeting and signal\_targeting\_groups.
## 5.875.0 — October 3, 2026 at 10:00 PM UTC
* Apostra SDKs now verify signed V3 webhook deliveries with rotation support.
* People choosing Buy media on a domain with a recoverable Seller account can request Buyer access for review. Signup keeps the Seller account and storefront history intact and blocks a second standalone Buyer account from being created without an authorized invitation.
* When you edit a product in Inventory Components, you now choose its formats from the same checklist you use for a new draft, including 15-second, 30-second, or custom video lengths, instead of editing JSON. Saved formats keep everything else exactly as saved. A saved format your source no longer offers stays on the product unless you remove it. On a Google Ad Manager source, native can't be added yet, but a native format already on a product stays as it is. Nothing to change on your side.
## 5.874.0 — October 3, 2026 at 8:22 PM UTC
* When you edit a product in Inventory Components, you now choose its channels from the AdCP channel list instead of typing JSON, and the editor won't save a product with no channels. Older products saved with `audio` or `video` show them as Streaming audio and Online video. In both the editor and the create-product draft, the channel picker now shows how many channels you've chosen and lists them by name beneath it; open the picker to add or remove one. While a product has pricing options, the editor shows the delivery type those options set instead of a separate product-level choice. Targeting capabilities now appear read-only and are never sent, so an edit can't change them. Formats, placements, and pricing are still edited as JSON for now. Nothing to change on your side.
## 5.873.0 — October 3, 2026 at 7:20 PM UTC
* V3 `save_event_source` now accepts `sellerAccounts` on each event source: up to 20 pairs of a connected seller account's `accountId` (from `get` with `kind: "connection"`) and the `sellerId` of the pixel to use on it, such as a Meta dataset or Snap Pixel. Use it to choose the pixel on a seller account that has more than one. Saving records your choice without contacting the seller. `search`, `get`, and the save response list each seller account in `sellerAccounts`, with `status: "pending"` and `sellerId: null` until Interchange confirms the pixel on that account; the pixel you named is not echoed back. Changing the pixel on an account that already has a confirmed one keeps the confirmed pixel in use and in `sellerId`, with `status: "pending"`, until the new one is confirmed. Naming a pixel again for an account whose sync failed, expired, or was removed returns it to pending so it can be retried. A save that changes only an event source's seller accounts reports `updated`. An `accountId` that is not an active, connected account of yours fails that entry with `REFERENCE_NOT_FOUND`, and the source is not saved. Event sources saved without `sellerAccounts` are unchanged.
## 5.872.0 — October 3, 2026 at 5:34 PM UTC
* In Inventory Components, **Draft product** now accepts your selected suggested matches and opens the draft form in one click, replacing the separate accept and draft steps. The form asks for channels (buyers filter discovery by channel), guaranteed or non-guaranteed delivery, and pricing. Pricing offers only the models your ad server can book, such as CPM, vCPM, CPC, or flat rate on Google Ad Manager, or CPM alone if the source doesn't report its models. CPM and vCPM can be an auction with a floor or a fixed rate; for a CPM auction you can leave the floor blank and the sales agent derives one. Other models are fixed only, and fixed prices are in your storefront's settlement currency. Video formats take 15 s, 30 s, or a custom whole number of seconds, and formats are preselected when their size exactly matches your selected inventory or their kind matches the media it declares, such as video. Each saved product on the Products tab now lists its formats, for example "Image 728×90, Hosted video 15 s", so you can confirm what saved without opening the JSON editor. Native in-feed isn't offered on Google Ad Manager sources yet, and the form says so.
* `sync_event_sources` now returns each event source's own platform ID in `seller_id` for Meta, Snap, Pinterest, LinkedIn, Reddit, Universal Ads and OpenAI Ads storefronts. Previously these storefronts returned the ad account ID there. The new value is the Meta dataset ID, Snap pixel ID, Pinterest conversion tag ID, LinkedIn conversion rule ID, Reddit pixel ID, Universal Ads pixel ID or OpenAI Ads conversion event setting ID, matching AdCP. TikTok and Spotify already returned the source's own ID and are unchanged. If you read `seller_id` as the ad account, use the account from your request instead.
* In Inventory Components you can now reach every ad unit and placement your ad server has synced, not just the first 50. Choose the inventory type to browse (for Google Ad Manager, placements or ad units), search by name or source ID, and load more from your ad server 50 at a time. Every row shows its source ID. Accept, hide and remap decisions and your selections stay in place while you search or switch type, so one product draft can combine components from different searches; switching to another ad-server source still clears them. Nothing to change on your side.
## 5.871.0 — October 3, 2026 at 4:19 PM UTC
* External AdCP buyers can create and update media buys on any commercially ready Apostra storefront without Scope3 first setting a spend cap for that buyer and storefront. Since 2 October these buys were refused with "This buyer is not provisioned for governed external AdCP campaign creation on the storefront", including buys a curated storefront placed with its underlying sellers. Where Scope3 has set a cap, its currency, per-buy limit and total exposure limit still apply. Where none is set, the seller's own terms, payment authority, rate card and approval policy decide.
* Uploading an image or video from the Creative Library in the web app now completes instead of stopping with a storage error.
* When an Apostra buyer requests proposals from a seller on AdCP 3.2, the seller now also receives the campaign's primary cost goal as a structured `criteria.outcome_target`, if it declares `media_buy.outcome_target` and accepts a media-buy `cost_per` cap in `features.bidding_policy`. The target names the goal metric and the most the buyer will pay per result (`cost_per` with `strength: cap` in the buy currency). It never carries a volume or the campaign budget. Sellers that don't declare both keep receiving the goal in the brief text only.
* Pages in the web app now always act on the account the web app shows, even after you switch accounts in Claude or ChatGPT.
## 5.870.0 — October 3, 2026 at 2:49 PM UTC
* Opening a Creative Library link from chat now lands on that advertiser's library even when your advertiser picker is set to All advertisers.
* Inventory capabilities for an ad-server source now list the pricing models that source's ad server can book, in `pricingModels`. Before, the field was always missing, so agents and product forms couldn't tell which pricing models a source accepts.
* If you build your own sales agent, the launch checklist and the Sales Agent best practices now ask that every Product declare its AdCP `channels`, such as `display`, `olv`, `ctv`, or `streaming_audio`, or `applies_to_channels` on each format option. Live-path certification now reports a Product that declares neither as skipped with the reason `missing_channels`, and a format that declares no channel on an otherwise per-format Product as `missing_channels:`; before, both were left out without a reason. It also certifies a Product whose formats declare `applies_to_channels` on those channels, where before only the Product's own `channels` counted. A missing channel is still only a warning and never blocks your agent. Channels will become required for external agents' Products in a later change, announced before it takes effect.
## 5.869.0 — October 3, 2026 at 12:56 PM UTC
* A campaign can now keep its own property lists on top of the advertiser's. Set `campaignPropertyListId` (include) or `campaignPropertyListExcludeId` (exclude) on `save_campaign` or on the v2 API's `POST /campaigns/{campaignId}/property-lists`. The campaign stores them, its active media buys are updated, and every buy launched later carries them too. Exclusions add to the advertiser's, and a campaign cannot drop an advertiser exclusion. An include list narrows the advertiser's include list, so a site must be on both. `get(kind: "campaign", include: ["propertyLists"])` shows the campaign's own lists (`campaignLists`) and the combined lists in effect (`effective`).
* **Breaking change on both `save_campaign` and the v2 endpoint:** `propertyListId: null` is now refused with a validation error. It used to remove property-list targeting from the campaign's live buys, leaving them without the advertiser's lists. To clear a campaign's own list, send `campaignPropertyListId: null`; the advertiser's lists keep applying. `propertyListId` itself is deprecated in favour of `campaignPropertyListId`: until 2026-12-31 a non-null value keeps doing what it did (it applies one of the advertiser's include lists to the campaign's active buys) and responses include a notice naming the replacement. After 2026-12-31 it is refused.
* When you edit a product in Inventory Components, the save now only sends fields your ad-server source can store. Video placement types are shown read-only, so the editor no longer reports a change as saved when the source kept the old value. Pricing options now show and save only the fields the source accepts: model, currency, delivery type, fixed or auction, rate, price guidance (which holds an auction option's floor), and minimum spend. The source still assigns each option's ID, and you can see the IDs under "Pricing options as the source reports them". Nothing to change on your side.
* Inventory components in Murph now uses the same card, header, tabs, table, buttons and form fields as your other storefront pages. What you can do there is unchanged: you still review, accept, hide and remap inventory components, draft and validate a product from them, and edit, archive or delete saved products in place. Nothing to change on your side.
* If you add your own signal targeting groups to a package for a seller-composed product, those groups now reach the seller's ad server alongside the product's own signals. Before this fix, they could be replaced by the product's signals without any error, so a buy ran without the targeting you asked for. Your groups and the product's groups must all match, so your groups narrow the product's audience. A package whose `signal_targeting_groups` is not a group set (root operator `all` with a list of groups) is now refused with an `invalid_signal_targeting_groups` error that names the field, instead of being dropped. Nothing to change on your side.
* Direct Storefront connections in Claude no longer offer a creative upload that would stop before the file reached storage. In Claude, upload through the Buyer V2 or V3 connector instead. ChatGPT Storefront connections keep the upload.
## 5.868.0 — October 3, 2026 at 11:32 AM UTC
* Products a storefront composes to your brief now declare their AdCP `channels`, such as `display`, `olv`, or `ctv`. Each lists every channel declared by the seller inventory it combines. You can filter them by channel in discovery, and their delivery is reported by channel. A composed product declares a channel only when the seller's inventory does, never guessed from its creative formats. Composed products you discovered before this change show their channels the next time you discover that storefront.
* Widgets in ChatGPT now receive their declared content policy, so ChatGPT loads the widget's images and files from our domains instead of blocking them.
## 5.867.0 — October 3, 2026 at 10:59 AM UTC
* Sales-agent certification requests in Murph now reach the Scope3 team to arrange a live test buy instead of entering the retired fixture evaluation path.
## 5.866.1 — October 3, 2026 at 8:21 AM UTC
* Resolve Storefront-owned sales-adapter routes before creative delivery so approved creatives reach the exact active source and blocked draft buys can continue once their matching acceptance is recorded.
## 5.866.0 — October 3, 2026 at 3:47 AM UTC
* `request_proposals` now honors seller targeting from two sources: the `sellerIds` field on the call itself, and the seller list set on the campaign via `save_campaign`. Existing calls that supply `sellerIds` now narrow the round to that list. When both are provided they are intersected — only sellers in both are contacted. When only `sellerIds` is provided on the call those sellers are contacted directly. An empty list is rejected. When `sellerIds` is supplied, any requested seller that is unavailable or outside a campaign list or authorized seller scope refuses the whole round with `CAMPAIGN_SELLERS_INELIGIBLE` and contacts nobody. In contrast, a campaign-only seller list, with no `sellerIds` on the call, dispatches to its eligible subset and reports unavailable campaign sellers. To broadcast to all eligible sellers, omit both selectors and pass `confirmBroadcast: true`. The result reports the applied cohort and bounded seller IDs contacted.
* VAST and DAAST tag buys now send sellers a hosted wrapper that records impression, click, and playback progress events.
## 5.865.0 — October 3, 2026 at 3:17 AM UTC
* Forward canonical package selections to legacy inventory sources using their persisted legacy format references when the selections cannot be completely resolved.
* An advertiser with no markets set is now treated as working in every market, and one with no channels set as buying every channel. Product requests and media buys for that advertiser reach every Market Maker that qualifies in any market or channel, instead of no automatic sellers at all. Seller accounts are still not created ahead of time for an advertiser whose markets or channels are blank; set them to narrow automatic selection to where the advertiser operates.
## 5.864.3 — October 3, 2026 at 2:36 AM UTC
* If you have set a price on a signal, products the Merchandising Agent builds with that signal now include it. The surcharge is added to the product's price floor: a product on inventory sold at 5 EUR CPM that uses a +1 EUR CPM signal is quoted at 6 EUR CPM or more. Before this release, signal prices were saved but left out of quotes. Signals a product includes together are charged once at the highest surcharge, so three +1 EUR segments add 1 EUR, not 3. A buyer discount cannot reduce the surcharge. A surcharge in a different currency from the product, a flat, per-unit or custom fee, or a price that is not a valid AdCP pricing option is not added to the quote, and the intelligence run's pricing breakdown records why. Quotes built after this release include the surcharge. Nothing to change on your side.
* Delivery reporting now converts cross-currency spend at the rate locked onto the media buy when you booked it, taken from the storefront you booked the buy with. Previously, buys whose storefront assigned its own media buy id missed their locked rate and were converted at the rate of the day for the buy's creation date instead, reported as `deliveryFxConversion.source: "snapshot"`. Those buys now report `source: "booked"` with the locked rate and its date, so their converted spend can move by the difference between the two rates. When we cannot tell which storefront a buy was booked with, it keeps using the rate of the day rather than borrowing another storefront's locked rate. Same-currency buys are unchanged.
## 5.864.1 — October 3, 2026 at 1:05 AM UTC
* Creative delivery now reports missing image, HTML5, or VAST destinations per Creative and warns buyers before seller delivery.
## 5.864.0 — October 3, 2026 at 12:18 AM UTC
* Keep a connected Library file in one document when its provider name or content changes. Renames now update the document name, and content changes add a new revision instead of creating a duplicate document.
* When this delivery origin is enabled, browser video players can fetch VAST we host with credentials: our VAST delivery endpoints return the calling page's origin with credentialed CORS and answer preflight requests.
* When your campaign's primary goal is on viewability or video completion and has a target, you can now see before booking how each product is expected to perform against it. `goalCoverage` on proposal goal answers, on the proposal-less products `request_proposals` returns, and on every product auto-select picks now carries `expected`: the product's expected rate from the seller's forecast (low, expected and high), the rate your target needs, and a verdict of `likely`, `possible` or `unlikely`. For example, "expected 78%-88% viewable vs your 70%". A cost per completed view or per viewable impression is judged at the product's CPM. Auto-select now prefers products where your target is likely, then possible, then products with no forecast range, then unlikely, among products that can optimize to your goal, and keeps its usual score or price order within each group. It never drops a product for this. Wholesale-priced products now keep the seller's viewability and completion ranges in their forecasts. Click goals get no expectation, because click-through rates do not carry over between advertisers.
## 5.863.0 — October 2, 2026 at 11:28 PM UTC
* You can now save a property list without applying it to every campaign. Create it with `appliesToAllCampaigns: false` on `save_property_list` (or `POST /property-lists`): the list is saved for the advertiser, but the advertiser's current lists and active media buys stay as they are. The default, `true`, works as before: the new list becomes the advertiser's list for its purpose and is applied to every campaign. `get` and `search` show which kind each list is.
## 5.862.0 — October 2, 2026 at 9:11 PM UTC
* Fixed media-buy updates so an unknown governed media buy identifies the requested media buy and confirms that no change was sent. FreeWheel sandbox cancellations now release booked capacity when no upstream campaign is configured.
## 5.861.1 — October 2, 2026 at 6:32 PM UTC
* Restore proposal-only Storefront media buys and MCP validation coverage after #17903 made durable buyer-campaign prerequisites universal.
## 5.861.0 — October 2, 2026 at 6:00 PM UTC
* Creative uploads from the web app's Creative Library and from Murph now finish. Before this fix, the upload stopped after preparing, with "The creative did not reach private storage." Choose the file again to upload it.
* If you upload avails feeds to a modular inventory source, give every row an AdCP channel, such as `display`, `olv`, `ctv`, `linear_tv`, or `streaming_audio`. Use a `channel` column, or a default channel on the request. Rows with a channel that isn't an AdCP code, such as `broadcast` or `Home Screen`, are now rejected with `INVALID_CHANNEL`. Choose the AdCP channel that describes how the inventory is delivered: broadcast television is `linear_tv`, and a connected-TV home screen is `ctv`. `CTV`, `Connected TV`, `video`, and `audio` are still accepted and stored as `ctv`, `olv`, and `streaming_audio`. A row with no channel is accepted with a `CHANNEL_MISSING` warning for now. We will start rejecting those rows after a separate announcement, at the same time as products created without channels. Rows you have already committed keep working. The publisher avails templates now use AdCP channel codes.
## 5.860.10 — October 2, 2026 at 5:38 PM UTC
* When your administrator publishes the versioned Microsoft Teams or Microsoft 365 Copilot package, the app presents Apostra with the registered Apostra mark. Existing app registrations, permissions, and conversation bindings stay in place; your administrator updates the existing app instead of creating a new one.
## 5.860.9 — October 2, 2026 at 4:59 PM UTC
* When a seller's agent rejected a creative on a media buy that was still waiting to start, we could keep sending the same creative to that agent every minute or two. We now stop sending it once the agent has rejected that version for that media buy. Sellers' agents no longer receive the repeated submissions. Buyers can still send the creative again with `sync_creatives`, and an edited creative is delivered as usual.
## 5.860.7 — October 2, 2026 at 3:12 PM UTC
* Media-buy updates forwarded to a seller's agent no longer carry the storefront's own `revision` number. That number tracks the storefront's copy of the buy, not the seller's, so an agent that checks `revision` could reject a valid update or compare it against the wrong version. Updates now arrive without a `revision`.
## 5.860.6 — October 2, 2026 at 2:55 PM UTC
* Release update notifications now call the product Apostra. The deployment summary and the "what's new for you" message you receive when a release ships previously still said "Interchange"; they now match the Apostra name used across the rest of your experience.
## 5.860.5 — October 2, 2026 at 2:38 PM UTC
* Embedded sales agent 1.52.0: Google Ad Manager products can now apply an advertiser's site and app lists through your GAM `domain` key-value. An exclusion list becomes `domain` is-not targeting on the line item, alongside the product's own key-values. This keeps your products eligible for advertisers with property lists once Interchange starts sending them, and it needs a custom-targeting key named `domain` in your GAM network. Buyers on AdCP 3.2-rc.7 can now connect; 3.1-beta.5, which had stopped working, is no longer advertised. Deactivated tenants no longer run scheduled syncs.
## 5.860.4 — October 2, 2026 at 2:15 PM UTC
* Asking your agent or Murph what you sell now includes the products your modular inventory sources offer, next to the products you author on an ad server. Each product shows its price, and Demo Storefront sample products are labelled synthetic.
## 5.860.3 — October 2, 2026 at 1:49 PM UTC
* Embedded sales agent 1.51.0: add Reliable Reporting Core, publish AdCP channels and applies\_to\_channels on every product.
## 5.860.2 — October 2, 2026 at 12:47 PM UTC
* Rate-card previews now recognise fixed prices from managed ESA products, and matching hard floors apply when proposals use those products.
## 5.860.0 — October 2, 2026 at 11:53 AM UTC
* External AdCP buyers may buy on any commercially ready seller storefront, including Meta and sales-agent storefronts. Sellers retain control through their terms, payment authority, rate cards, and approval rules. Distribution gates public discovery only.
* Organization administrators working in one of the organization's accounts now get the organization's full Plan & billing page right there, without switching accounts. Payment methods, plan selection, contracts and media billing changes apply to the organization. Payouts stay per storefront: the Payouts tab shows the account's own payouts, notes that the organization manages them, and offers a switch to the organization to change them. Older billing links and the setup checklist's Payment & billing row now open Plan & billing in one step. Media billing requests made from one of the organization's accounts now read and change the organization's media billing entities, and require administrator access to the organization. No action is required.
* External AdCP buyers can continue updating historical route-based media buys while seller storefronts retain the same Distribution profile selection and approval controls.
## 5.859.2 — October 2, 2026 at 11:30 AM UTC
* On a Demo Storefront practice run, saving coaching in Proposal Pass and running the test again now visibly changes the proposal's next ask, as the guided rehearsal does. Before, a rerun started from the page lost its practice settings, so the coached change never appeared.
* On the Media Partners page, a media partner you haven't selected now shows the billing that would apply when you select it, instead of reporting its billing as unsupported. The consolidated or direct choice and **Approve direct billing** appear once you select the partner. A partner that declares no billing Apostra can use shows **No supported billing** instead of claiming consolidated billing. Agents reading sellers through the API see the same billing policy. Nothing to change on your side.
## 5.859.1 — October 2, 2026 at 10:59 AM UTC
* Business Manager plans now open for review and acceptance. The plan review no longer fails with "Couldn't load IU pricing" when a plan's entitlements grant only an account limit, and it shows that limit, such as up to 100 accounts under the organization.
## 5.858.5 — October 2, 2026 at 7:32 AM UTC
* The Connect support settings now say notifications route "from your account" instead of "from your storefront," so the copy reads correctly for buyer accounts as well as sellers.
## 5.858.4 — October 2, 2026 at 7:08 AM UTC
* Creatives added to active campaigns now continue to seller delivery after the seller approves the media-buy update. Buyers do not need to change their upload or approval workflow.
## 5.858.3 — October 2, 2026 at 6:38 AM UTC
* A new Demo Storefront now reads "Paused before going live" in the seller rail and the account switcher, instead of "Live — critical". Its sample confirmed booking is synthetic, so it no longer counts as a real transaction when we decide whether a Storefront has gone live. The sample booking and its delivery still appear on Media buys, Demand, and the Demo Storefront page. Storefronts with real transactions see no change.
* Proposal Pass no longer leaves its feedback, revision and proposal-file buttons disabled when you open another turn while a save from the previous turn is still finishing.
## 5.858.2 — October 2, 2026 at 6:08 AM UTC
* Synthetic inventory sources in an active Demo Storefront no longer show an "Ad Server Configuration" blocker in the seller rail or on the synthetic managed source. That source has no real ad server by design, so its ad-server connection step now counts as complete while the Demo is active. Every other setup task still reports normally, and sellers with real ad servers see no change.
## 5.858.1 — October 2, 2026 at 5:47 AM UTC
* Proposal Pass now keeps checking a requested proposal file (JSON, HTML, PDF or PowerPoint) for up to five minutes, and every new request starts its own check. A file that takes a few minutes to render now shows as ready on its own, instead of stopping at "Live updates paused" and waiting for you to press Refresh.
## 5.858.0 — October 2, 2026 at 4:58 AM UTC
* Sellers can upload rate-card documents that apply only to a matching buyer.
## 5.857.0 — October 2, 2026 at 3:11 AM UTC
* When your campaign has optimization goals, you can now see before booking which goals each product cannot optimize to. Proposal goal answers, the proposal-less products returned by `request_proposals`, and every product auto-select picks carry a `goalCoverage`: whether the product can optimize to your primary goal, and each goal it cannot, with a reason code. Auto-select now prefers products that can optimize to your primary goal, ranking them ahead of products that cannot and keeping its usual score or price order within each group. It still picks a product that cannot when the budget reaches past every product that can. Sellers still receive your brief, you can still book any product, and a product that declares no optimization capability counts as unable to optimize to any goal.
## 5.856.0 — October 2, 2026 at 2:17 AM UTC
* Apostra SDKs now expose typed human-handoff helpers that retain the returned URL, expiry, and safe resume instructions.
* Sellers can add an avails sheet to the Library, check its Inventory Feed preview, and commit accepted availability rows to a modular source.
* Changing a Meta media buy's start or end date now verifies that Meta actually applied the change before reporting success. Previously, a flight-date update could be reported as successful even when Meta silently did not apply it, because nothing compared Meta's response back to what was requested — unlike budget and targeting updates, which already do this check. A date change Meta doesn't apply now returns an error instead of a false success.
* Updates to media buys now reuse the account accepted when the media buy was created. Buyers do not need to send an account again when changing a budget or schedule.
* Fixes AI-9719
## 5.855.1 — October 2, 2026 at 1:30 AM UTC
* When a seller refuses your requested payment terms, the account request now reports the refusal instead of opening an account on the seller's default terms. Accounts already accepted by the seller keep their agreed terms and remain usable.
* Notification emails now declare a light-only color scheme so dark-mode mail clients preserve the Apostra palette instead of force-darkening the template.
* Storefront media-buy routes with an immutable FX lock now have their stored buyer currency repaired from that lock, so seller-account money views do not label buyer-currency spend as USD when the buyer transacted in another currency.
## 5.855.0 — October 2, 2026 at 1:13 AM UTC
* Apostra now signs its AdCP calls to A2A sales agents as well as MCP agents, for every operation the agent lists in `request_signing`. The inventory source page no longer warns A2A agents that their calls arrive unsigned, and the authentication docs no longer limit signing to MCP. An A2A agent that required signatures can stop accepting a fallback credential from Apostra once it sees our signed calls verify.
* Buyer agents can record the payment terms sellers receive for consolidated billing when payment-terms rollout is enabled.
* If your inventory source is configured in Apostra as selling wholesale but your agent does not declare `wholesale` in its buying modes, Apostra now follows your agent's declaration. It stops requesting your wholesale catalog and shows the catalog as empty, so your wholesale products no longer appear to buyers until you declare it. Your source is marked degraded, and its diagnostics name the fix: add `wholesale` to `media_buy.buying_modes` in `get_adcp_capabilities`. The next catalog refresh after that fills the catalog again. Buyer briefs still reach your agent, and a storefront that is already live stays live.
## 5.854.0 — October 1, 2026 at 11:57 PM UTC
* Apostra SDKs now provide cached OAuth client-credentials token providers for deployed backend integrations. Existing credential options continue to work unchanged.
* Organization administrators now find API keys and Advertisers under Organization in Account settings when working in the organization, and administrators of a standalone account find both in their Account settings. The API keys and Advertisers pages open the matching settings pane directly. Child accounts and members who are not administrators keep what they use today. No action is required.
## 5.853.1 — October 1, 2026 at 11:23 PM UTC
* Restore Agent behaviour baseline evaluations so Agent pages receive current Source baseline results.
## 5.853.0 — October 1, 2026 at 11:09 PM UTC
* Apostra SDKs can now retain a successful call's server request ID and response headers through generated response variants. Existing method calls still return only data.
* Create Meta media buys with supported Facebook signal audiences.
## 5.852.0 — October 1, 2026 at 10:42 PM UTC
* Apostra SDKs now retry transient requests safely and provide receipt settlement helpers for caller-keyed writes.
## 5.851.1 — October 1, 2026 at 10:08 PM UTC
* Prevent AGENT-source creative syncs from sending create-only package fields.
* Read Meta flexible audience specifications back as signal targeting groups.
* When an organization administrator opens their organization, it now opens Organization members in Account settings instead of a separate organization settings page. Older Organization settings links go straight to the matching Account settings pane. Organization members who are not administrators keep their organization dashboard. No action is required.
## 5.851.0 — October 1, 2026 at 9:15 PM UTC
* Apostra SDK clients now read their API key, account ID and base URL from environment variables by default, so a status check can start with no constructor or empty input arguments.
* A Seller Account created without choosing a seller product now starts on the one your organization's plan covers. On an Agentic Media Company plan it starts as Agentic Media Company instead of being refused; on a Listing plan, or without a paid plan, it starts on Just list as before. A seller product you choose yourself is kept, and is still refused when your plan doesn't cover it.
* Pre-submit creative syncs for draft buys no longer send an invalid account to agent inventory sources before a source route exists.
* Adding, moving, restoring, or switching an account now checks your organization's plan first. If the plan doesn't cover the change, Apostra refuses it before anything is created and tells you which plan it needs and what to do next. For example, adding a Buyer Account to a Listing organization asks you to switch the seller product to Agentic Media Company, then choose an Agentic Media Company plan. Organizations without a paid plan can still add any account. The Account Access and Signup guide lists which plan holds which accounts.
## 5.849.0 — October 1, 2026 at 7:43 PM UTC
* Use `@scope3/agentic-contracts/apostra` and `@scope3/agentic-contracts/apostra/sessions` for canonical Apostra contract names. Existing Murph package imports continue to work unchanged.
* Google Drive folders shared with your connected account now keep their Library content current when files are renamed, moved, edited, or deleted. Nothing to change on your side.
* Buyer Setup now lets an organization administrator choose the payment terms Apostra asks new seller accounts to accept. The default is net 60, but it takes effect only after the administrator saves it. Longer terms can take longer to approve or be rejected because sellers carry the credit risk.
* If your agent requires an account for `get_products`, the source discovery test now sends one instead of failing with "account required". The test buys as a test persona shaped like real demand: brand `test-brand.apostra.com`, operator `test-agency.apostra.com` (the agency holding the seat), and Apostra as the agent. It first registers that account with `sync_accounts`, using a billing mode your agent supports. When your capabilities declare `account.sandbox: true`, the account is marked as a sandbox account; otherwise the test result says it ran on a live account. Either way it sends only a no-spend `get_products`. Agents that assign their own account IDs still get the account-required result, since the test has no operator credential to use.
* When creating a Meta draft with `save_media_buy`, set `budgetAllocation.mode` to `seller_optimized` for one shared campaign budget or `fixed` for separate ad set budgets. If you omit the mode, the tool asks you to choose before creating the draft.
* Keep creative sync working when a buy's selected products differ only by legacy video dimensions.
* Scope3-funded Creative Engine voice generation now reaches the selected voice provider instead of failing before generation begins. No change is needed to buyer-provided engine connections.
* If Apostra invites you to administer a new Buyer Account with a custom IU plan or discount, your account invitation can arrive with a Commercial Offer already prepared for your review. Verify the invited email and join the account, then review and accept the Offer separately. Receiving the invitation does not accept its price or subscription terms for you.
* When you reset a password in Apostra, the recovery screens now match the sign-in experience from the first reset request through confirmation. Nothing changes about how you request or complete a password reset.
* Murph now recognises more ways of sharing a secret in chat and sends you to the secure credential form instead. That covers API keys, plurals such as "passwords" or "tokens", and wording such as "I'm sending" or "I pasted". Before, a message like "I'm sending my API key now" didn't trigger the secure-form redirect.
* Embedded sales agent 1.50.0: classify ad-server failures for buyers and log vendor detail for operators, serve AdCP 3.2-rc.7 on adcp 8.0.0rc3.
* If you create or read event sources through the V3 API, use `save_event_source` and `search`/`get` with `kind: "event_source"`. The duplicate `save_measurement_source` tool and the `measurement_source` search and get kind are removed from V3 MCP, the V3 HTTP API, and the Apostra SDKs. Calls to them now return an unknown-tool or invalid-kind error. Your event sources and the campaign goals that reference them are unchanged: an event source's `eventSourceId` is the same value on both paths, so existing campaign optimization goals keep resolving. The V2 REST event-source and measurement-source endpoints are not affected.
* When a storefront's creative library held two versions of the same creative, the storefront could alternate between them, creating a fresh delivery for each about once a minute and sending callbacks for the older version's operation. That alternating has stopped, so your callback endpoint no longer receives those repeated callbacks. Submitting creatives with `sync_creatives` works exactly as before.
* Rate cards no longer show a selling-point suggestion containing their prices.
## 5.848.0 — October 1, 2026 at 5:26 PM UTC
* Campaign creative resync now continues for eligible media buys when another buy is still waiting for a route. You can also archive failed, canceled, and rejected media buys without sending another seller cancellation.
* Rate-card hard floors now apply to every price, and the rate-card preview says when a row will not apply.
* Connected Library sources: **Sync now** now also retries files that failed to import, instead of leaving them until the file changes in Google Drive, SharePoint or Confluence. Use it after fixing whatever stopped the import, such as restoring access.
* A product that declares no optimization capability no longer receives optimization goals on its package. The seller still sees every goal in the brief, and each withheld goal is reported in the `optimization_goals_dropped` execute warning with its reason.
## 5.847.0 — October 1, 2026 at 4:27 PM UTC
* Apostra SDK writes now align generated body idempotency fields with the caller-owned request key.
* Apostra SDK errors now provide typed classes, stable codes, recovery guidance, retry metadata, and request IDs.
* The **Archive account** row in a buyer account's **Settings → Danger zone** showed a button that could never be used and told you to contact support. Organization administrators now get **Open organization accounts**, which switches to the organization and opens its account list, where you can archive the account. Other members of the account see that an organization administrator archives it. Standalone buyer accounts no longer show a Danger zone, because only accounts inside an organization can be archived.
* Keep legacy creative-format routing available for wholesale media buys when a legacy seller has no projection catalog.
* When Apostra rejects a LinkedIn `create_media_buy` or `update_media_buy` before sending anything to LinkedIn, the error now carries `details.mutation_outcome: "not_dispatched"`. Retrying such an update with the same idempotency key now runs it again instead of replaying the saved failure; a create retry does the same when the error is correctable (`recovery: "correctable"`). Errors after LinkedIn received a change keep the key and replay as before. Nothing to change on your side.
## 5.846.0 — October 1, 2026 at 3:04 PM UTC
* When a sender texted your agent phone line again while the agent's reply to them was being sent, that reply could fail and be marked with an unknown delivery outcome, even though it was never handed to the carrier. The reply now sends normally and the new text is still received. Texts from other senders to the same line are also no longer held up while a reply is being sent. Nothing to change on your side.
* Archiving a sandbox advertiser now releases its brand identity, so you can create a new sandbox advertiser for the same brand, countries, and currency instead of restoring the archived one. Only one active sandbox advertiser can hold a brand identity at a time: restoring an archived sandbox advertiser whose brand identity another active sandbox advertiser now holds returns `409 CONFLICT` with `reason: brand_identity_exists` and that advertiser's `advertiserId`. Non-sandbox advertisers are unchanged: an archived one still holds its brand identity.
* Apostra app and agent icons now use the centered official apostrophe mark.
* The app now defaults to the Apostra-branded presentation without requiring a product-brand flag, gateway hostname, query parameter, or local preview setting.
* Meta media buy creation now rejects a product that is still in testing, even when the buyer holds a product ID from an earlier discovery. The request fails with `UNSUPPORTED_FEATURE` before anything is sent to Meta, so no campaign is created. Select a product returned by a current `get_products` call instead.
* When you open Plan & Billing from inside a Seller Account that belongs to an organization, it now says that billing and payouts are managed by the organization, by name. If you administer the organization, **Open billing** takes you to the organization's billing, where you can make changes; in the web app this also switches you to the organization. If you don't, the page names the organization and says that one of its administrators makes these changes.
* Proposals now use the seller's public listing name.
## 5.845.0 — October 1, 2026 at 1:29 PM UTC
* Connected Library sync now skips empty Google Drive files safely and continues importing healthy files when one file needs a retry.
* When you try to change payouts or billing from a Seller Account that belongs to an organization, the refusal now names the organization that manages billing, for example "Payouts for Azerion are managed by the Azerion Admin organization." If you administer that organization, it tells you to switch to it; otherwise it tells you that one of its administrators has to make the change. API and agent integrations also receive the organization and the next step in the error details.
* Embedded sales agent 1.49.6: let the GAM write canary traffic as booking does when no trafficker is set.
* If you create products on a managed inventory source, include `channels` on every new product, such as `display`, `olv`, `ctv`, or `streaming_audio`. `save_wholesale_product` and the product API now warn when a new product declares none, and we will start refusing those creates after a separate announcement. Any channel code that isn't an AdCP channel is refused now, on creates and on changes. Changes to existing products work as before: leave `channels` out to keep the ones a product has, or send a list to replace them. Declared channels are how buyers find your products in channel-filtered discovery, send you the right proposal requests, and see your delivery by channel. Products saved before this change keep working as they are.
* Breaking change on the v3 MCP endpoint: in media buy live status (`get` with `kind: "media_buy"` and `include: ["liveStatus"]`), `liveStatus.adcpMediaBuyId` is renamed to `liveStatus.storefrontMediaBuyId`. The old field is no longer returned, so agents that read `liveStatus.adcpMediaBuyId` must switch to `liveStatus.storefrontMediaBuyId`. The value is the same: the storefront's id for the buy, `null` until the buy is submitted. The rest of the live status is unchanged. The v2 REST and MCP APIs do not change; Get media buy status still returns `adcp_media_buy_id`.
## 5.843.0 — October 1, 2026 at 12:04 PM UTC
* Murph now answers questions that mention credentials instead of replying only with a link to the secure credential form. Before, asking something like "what auth type does my sales agent use?" alongside words such as "send" or "enter" could get every answer replaced with that link, even when you weren't trying to share a secret. Murph still sends you to the secure form, and won't take the secret in chat, when you offer to share a password or token with it or paste one into the conversation.
* Library responses show the document passage or rate-card row behind each block.
## 5.842.0 — October 1, 2026 at 11:35 AM UTC
* Add notes to the Library for information that is not in a file. Give a note a short title and save it for your storefront agent to cite. Open a note to see its author, date, and current revision.
* Embedded sales agent 1.49.5: when a buyer updates a Google Ad Manager media buy, the line item's custom targeting is rebuilt from the product's own targeting keys plus the buyer's new targeting. Before, an update could drop the keys that define the product, so the line item delivered more broadly than what was sold, and criteria the buyer removed could stay on. HTML5 ZIP creatives now upload to Google Ad Manager as native HTML5 creatives.
## 5.841.0 — October 1, 2026 at 11:09 AM UTC
* When an external sales agent needs a credential or an OAuth grant, Murph and V3 agents are now pointed to that source's own connection form, where you can enter it. Before, they were pointed to the ad-server connect page (Google Ad Manager, FreeWheel and so on), which can't reach a sales agent, so you were left with nowhere to enter it. A sales agent saved with "No auth" is still treated as connected and is never asked for a credential.
## 5.840.0 — October 1, 2026 at 10:30 AM UTC
* Embedded sales agent 1.49.4: apply only the sent fields in the SpringServe, FreeWheel and AdsWizz settings PUTs.
## 5.839.4 — October 1, 2026 at 10:01 AM UTC
* Talpa's approved 30-second audio creative can reach Triton again, and future catalogue refreshes keep its reviewed format route intact. The existing creative will be retried without changing its media or format selection.
## 5.839.3 — October 1, 2026 at 8:58 AM UTC
* Campaign creative syncs now include HTML5 display dimensions in the creative format parameters sent to managed storefronts. Existing ZIP-backed creatives can be retried without re-uploading, so approved campaigns no longer stop before ad-server delivery because width and height are missing.
## 5.839.2 — October 1, 2026 at 8:09 AM UTC
* Changing a media buy's start or end date now waits for the seller to confirm the change before reporting success, matching the existing behavior for budget and targeting updates. Previously, a flight-date change could be reported as successful immediately, before the seller had accepted it — so a rejected update (for example, a date change on a paused campaign) could leave the live campaign unchanged with no error surfaced. A rejected date change now returns an error instead of a false success.
## 5.839.1 — October 1, 2026 at 7:11 AM UTC
* Approved creatives for an unsubmitted media buy now reach its selected seller source before the buy is sent. Existing drafts recover automatically, and buyers do not need to recreate them.
* Fix Meta create/update media buy and region/city targeting search intermittently failing with a generic upstream error. Meta rewrites the pagination continuation link it returns to its current default API version regardless of the version the request used; the adapter was rejecting that link as untrusted instead of following it.
## 5.839.0 — October 1, 2026 at 5:10 AM UTC
* Buyers can generate and save approved Creative Engine outputs directly to an advertiser's Creative Library without creating a campaign first.
## 5.838.0 — October 1, 2026 at 3:56 AM UTC
* The Apostra TypeScript and Python SDKs now include generated guidance for every operation and documented field, helping developers and agents understand API calls directly in their editor.
## 5.837.0 — October 1, 2026 at 3:33 AM UTC
* The Apostra TypeScript and Python SDKs now use stable, meaningful V3 schema type names instead of positional generated aliases.
## 5.836.1 — October 1, 2026 at 3:04 AM UTC
* Apostra Buyer invitation emails now link to the quickstart guide, so invited teams can start setting up their account after joining.
## 5.836.0 — October 1, 2026 at 2:50 AM UTC
* A creative now attaches to a product that offers its format at several sizes or lengths, as long as its uploaded asset matches exactly one of them. Before, a 1920x1080 video sent to a product offering 1920x1080, 1280x720 and 640x360 video was rejected as "ambiguous canonical identity matches 3 distinct product options", even when you also passed the matching format. If the asset matches none of the options exactly, or hasn't been measured yet, it is still rejected and you can name the option with `format_option_refs[]`.
* When your campaign has optimization goals, each seller package now carries only the goals that product says it can optimize, so a goal a seller cannot act on no longer causes that seller to reject the whole buy. Goals left off a package are listed in an `optimization_goals_dropped` warning when the campaign executes, with the product, the goal and why it was not sent. Sellers still see every goal in the brief, products that declare nothing still receive every goal, and nothing changes on your side.
## 5.835.0 — October 1, 2026 at 2:01 AM UTC
* Prices are now edited in your Library's Rate card document, and the Playbook shows the current rate card with a link to edit it.
* Validate V3 Creative Session saved and promoted results against the existing public contract.
## 5.834.0 — October 1, 2026 at 1:16 AM UTC
* Sales Agent owners can see the stored baseline declarations for Agent eligibility and whether property-list buys are eligible on the Agent Page.
* If Apostra has provisioned an agent voice line for your Seller Account, inbound calls to that number are now answered — nothing to set up on your side. Callers hear, before anything else, that they have reached an automated agent rather than a person, then the call ends; the line cannot carry a conversation yet. Apostra keeps a record that the call arrived, was answered, and was disclosed, but never stores the caller's number or the number they dialed. Call recording and transcription stay off, unchanged by this.
* Each step of the call is durable work, so a restart part-way through retries rather than abandoning a caller, and a retry never answers or speaks twice. If your line is turned off mid-call, the call is ended rather than left open and silent.
* Voice lines are in limited rollout and provisioned by Apostra — there is no self-service setup yet. Contact Apostra if you would like one for your storefront agent.
* Breaking change: the old one-source-per-call V3 `save_event_source` request no longer works. Update your calls to send one `advertiserId`, one `idempotencyKey`, and an `eventSources` list of up to 50 entries, each keyed by your own `eventSourceId`, to create, update, archive, or restore a source in a single call. `name` is still required when an entry creates a source, and `allowedDomains`, `integrationPlatform`, `mapping` and `testEventCode` are no longer accepted on V3. Each entry returns its own result, so one failed entry does not stop the rest, and failures come back as AdCP-shaped errors. The event source object also drops the separate `id` field, using `eventSourceId` as its only id, reports `health` in the AdCP `EventSourceHealth` shape, and lists `sellerAccounts` instead of a single `sellerId`; `sellerAccounts` is empty until seller-account sync ships.
* Storefront readiness now catches a sales agent whose billing declaration leaves buyers no way to pay. If your agent's AdCP capabilities declare `account.supported_billing` with only `advertiser`, or your agents declare billing models with nothing in common, readiness shows a `supported_billing_declared` blocker, and buyers can't connect to the storefront until it clears. To fix it, include `agent` (consolidated billing) or `operator` in the declaration (an agent that requires operator authentication must include `operator`), then refresh its capabilities from source diagnostics. The blocker clears on its own. Agents that don't declare billing at all use consolidated billing and need no change, and platform adapter storefronts are unaffected.
* Campaigns now follows your language. The campaign list, campaign details, statuses, filters, dates, amounts and error messages appear in German, Spanish, French, Italian, Japanese, Korean, Dutch, Brazilian Portuguese or Simplified Chinese, and switch when you change the language in Apostra, Claude or ChatGPT. Campaign names and other text from your data are shown as entered.
* Creative review callbacks no longer fire for a `sync_creatives` operation that a later submission of the same creative has already replaced. The outcome arrives on the callback of the newer operation, so your webhook endpoint stops receiving stale results for work it has already re-submitted. A callback your endpoint rejected permanently stays parked and is not re-sent.
* Proposals now draw on your Library documents for full-length briefs. Previously a passage had to contain every word of the brief before the agent could cite it, so most briefs found nothing to cite.
* Fix account-scoped bookings for complete-key seller accounts. These bookings no longer try to write a complete-key seller-account identifier into legacy numeric route fields.
* Media Partners no longer shows **Add media partner** on the card of a media partner you have already connected. The card now shows that connection's next step: **Choose account**, **Reconnect**, **Map advertiser**, or **Manage**. In an advertiser's view, **Map advertiser** and **Manage** reflect whether that advertiser is mapped.
* Agents can answer the same question: each result from `search(kind:"connection")` now includes `mappedAdvertiserIds` (up to 100 advertiser ids, ascending) and `mappedAdvertiserIdsTruncated` alongside `mappedAdvertiserCount`.
* Storefront product discovery no longer refuses a brief because it contains ordinary numbers such as "over 3 months", "up to 10 euro", "at least 12 months" or "500+ leads". Those briefs previously came back with no products and a request to clarify age targeting. A brief that states an age with only one bound, such as "people over 35" or "18+", still asks for both bounds before any product is returned.
* The storefront Buyers directory now loads month-to-date spend instead of showing unavailable values when delivery data is present. Currency handling and zero-spend filtering are unchanged. No action is required.
* Buyers using MCP assistants can now switch from an Organization to a Buyer Account and continue the same task without reconnecting.
* Agents can switch into an Organization account, check its status, and switch back to a Buyer Account. Signed-in administrators can also find Organization tools in an MCP connection that keeps its initial tool list. Each action still uses the permissions of the selected account.
* When a seller on AdCP 3.2 answers your cost goal, the proposal and the booked media buy now record the cost it can plan to. A 3.2 seller states that cost in `commercial_terms.bidding.cost_per`, so a cap of 3 it can only plan at 4.50 now shows as a best-effort answer of 4.50 that misses your ask, instead of an unanswered goal. Sellers on earlier AdCP versions read exactly as before.
* Opening Plan & Billing, or another organization-level page, no longer signs an organization admin out to the "Request access" page. The organization itself stays open so you can review billing or choose an account. Nothing to change on your side.
* When a linked storefront source sends a booking, update, delivery read, or creative request to a partner, the partner now sees your storefront as the buyer. Your buyer's account stays on your own booking and is not shared with the partner. Nothing to change on your side. If a partner requires its own buyer account, the booking returns a clear error instead of using your buyer's account.
* If you administer a buyer or seller organization with child accounts, you can now open **Account settings → Organization** from the organization or a child account to manage accounts and members and review the organization's billing and contract details without switching accounts. Members with access only to a child account see a notice naming the organization that manages billing; they cannot see its accounts, members, contracts, rates, or payment information. No setup is required.
* A V3 `request_proposals` round on a campaign with channel groups now asks only the sellers that can match those groups, so an audio-only campaign no longer contacts display-only sellers. Nothing to change on your side. A seller counts as a match when at least one channel it declares matches a group, and aliases match (a seller listed as `audio` matches `streaming_audio`). A seller that declares no channels is still asked, and official platform integrations use the platform's own channel coverage. Campaigns without channel groups, and rounds already running, are unchanged.
* Each seller asked receives the union of your groups' channels as its AdCP `channels` filter. Returned products whose channels or creative format kinds fit none of your groups are dropped, along with any proposal that allocates budget to them, and the seller's outcome reports the count as `channelExcludedProductCount`. Products that declare no channel or format are kept.
* The response lists eligible sellers that were not asked in `skippedSellers` (up to 25 named, with a `total`), each with the channels it declares and the reason `channel_mismatch`. If no eligible seller matches, the round fails with `NO_ELIGIBLE_SELLERS`. Seller narrowing applies only when every channel group names at least one channel; if any group names none, every eligible seller is asked and no channel filter is sent. A round whose one seller is fixed by your account, such as a sponsored-buyer or own-supply sandbox campaign, never skips that seller. To include a skipped seller, add a channel group for its channel and start a new round.
* Your coding agent can now prepare credential setup for an existing external buyer agent. Complete setup in **Settings → API keys → Manage buyer agents**, then store the one-time secret directly in your runtime’s secret store. Your coding agent receives only a non-secret reference and server-verified setup state.
* The reference is bound to the administrator, organization, and buyer agent, and expires after 15 minutes if unused. An active credential does not confirm that the runtime stored it or authenticated. Advertiser access still requires separate grants.
* A text that an agent line refuses before it reaches the carrier, because the recipient opted out after it was queued or the line is no longer active, no longer counts toward the line's rate or daily spend limit. Queued texts to opted-out recipients can no longer hold back the line's other texts.
* Apostra-managed sales agents on SpringServe and Triton now refuse a buy whose targeting they can't apply, instead of booking it and silently ignoring that targeting. This covers audience signals and signal groups (including exclusions), dayparts, devices, language, metro targeting outside Nielsen DMA, and other fields those ad servers don't support. A refused launch names the targeting the seller can't apply, so you can remove it or choose another seller. Nothing already running changes.
* Creative Session saves and generation work again after a storage change.
* Apostra-hosted applications retry a transient runtime configuration failure during startup before showing the secure configuration error.
* Sales-agent developers can now read Apostra's required declarations, live-behaviour standards, and integration best practices in one guide.
* Settings now always opens in place: the left rail switches to the settings navigation and the section you pick fills the conversation area. Previously the Settings gear on the collapsed rail opened a separate window over the page, while the Settings button on the open rail and every settings link expanded in place — the same screen behaved two different ways depending on where you opened it from. On a narrow screen the sections were a horizontal strip you scrolled sideways; they are now the same grouped list you see everywhere else.
* The "Receive Murph announcements" switch also now tells you when it fails. It used to move on click and stay moved even if the save failed, so the setting could read as on while it was still off. Now a failed save puts the switch back and shows an error.
* The inventory source page now shows how your agent handles request signing. **Technical details** reads the `request_signing` block from your agent's `get_adcp_capabilities` response and lists each category it declares: the operations it requires signed, the ones it checks without enforcing, and the ones it verifies when a signature is present. If it declares no signing, the row says so. Next to the authentication choices, the connection form now explains that request signing is not a credential and has no setting there. Apostra signs the operations an MCP agent lists; calls to A2A agents are not signed yet, and an A2A agent that requires signing now sees a warning on the page. The authentication docs now state this boundary. The source diagnostics API returns the same declaration as `source.capabilities.requestSigning`.
* Dimension save results now provide typed action, identity, and usage fields in the Apostra TypeScript and Python SDKs while retaining provider extension fields.
* If your storefront uses a third-party sales agent, Setup now shows a **Product channels declared** warning when some of its products declare no AdCP channels (such as `display`, `olv`, `ctv`, or `streaming_audio`), with a count for each agent. Buyers filter discovery, route proposal requests, and report delivery by channel, so those products are missing from channel-filtered discovery. The warning does not block your storefront or stop the agent from receiving requests. To clear it, ask the agent's operator to add `channels` to each product, or `applies_to_channels` to its formats. Channels will become required for external agents' products in a later change, announced before it takes effect.
## 5.833.0 — September 30, 2026 at 8:29 PM UTC
* Choosing your organization's payment terms is now a go-live step for sellers on consolidated billing. Until an organization administrator chooses them, buyer readiness shows an incomplete **Payment terms** check, those sellers show `needs_commercial_setup`, and a buy from one is refused with the `payment_terms` blocker. Direct-billed sellers aren't affected. Set `paymentTerms` with Update billing info (`PUT /api/v2/billing/info`), even if you want to keep the default. Once chosen, terms can be changed but not cleared: `paymentTerms: null` is refused. See "Your organization's payment terms" in Buyer setup.
* You can now cancel a single media buy with `save_media_buy` by sending `isCanceled: true`, and its campaign and the other media buys on it stay as they are. Cancellation can't be undone, and if the seller has to confirm it, the response says the cancel is pending seller confirmation.
* Keep package assignments when creatives are re-synced to an existing media buy, so seller-approved creatives reach the intended ad-server line item.
* Discovery now removes excluded publishers from multi-publisher products while retaining the remaining eligible inventory.
* Organization settings now shows active and archived accounts separately, keeps Add account visible to administrators, and explains when account creation is unavailable. Billing & contract can load while account setup is pending.
* When you sign up as a seller, the plan list now shows every public plan that applies to sellers, including the Storefront agent plans, instead of leaving them out. The plan you accept sets your starting seller product: a Listing plan, or no paid plan, starts you as Listing only, and a Storefront agent plan starts you as an Agentic Media Company. A paid plan you choose is accepted as part of signup once your email address is verified, and takes effect when signup completes. Only an accepted plan sets your starting product: if your email is not yet verified, or the plan you picked stops being offered while you are signing up, you finish on the free pass-through terms as Listing only and choose a plan again. Your account is saved with its product, so Plan & Billing shows the plans that match it after you sign up. You can change your seller product later in Settings. Nothing to change on your side.
## 5.832.0 — September 30, 2026 at 7:38 PM UTC
* The default payment terms for your organization are now `net_60` instead of `net_30`: until you choose, `net_60` applies and sellers are asked for it. On storefronts Interchange hosts or manages, only `net_60` is approved automatically; any other terms, shorter or longer, wait for the seller's review. See "Your organization's payment terms" in Buyer setup.
* In a Demo Storefront, the Product Marketing practice card no longer asks you to review or confirm proposed facts from the sample teaching documents. Proposals quote those documents directly, so there is nothing to confirm. After you prepare the samples, the card says they are still processing and offers the held-out synthetic buyer brief once they are ready. Nothing to change on your side, and your own documents are unaffected.
## 5.831.0 — September 30, 2026 at 7:27 PM UTC
* Campaign launches now identify the affected media buy when campaign targeting conflicts with a selected package and explain how to correct it. The same safe reason remains visible on the failed media buy, while raw seller error text stays hidden.
* Demo Storefront evaluations again open their synthetic buyer brief after the teaching sources are ready. The demo keeps its approval flow and all three synthetic inventory sources.
* Canonical VAST creatives now reach legacy sellers through managed storefronts as well as direct seller connections. No storefront changes are required.
* In the Generated variants gallery, select **Save to library** to add the selected, approved output to that advertiser’s Creative Library as a Creative Asset revision. **Use in campaign** remains separate, so saving to the library does not create or change a campaign Creative. If the library save is refused, the approved output remains available to try again. This is available to buyers enrolled in Creative Engines.
* Media buys for legacy-only sellers now retain the seller's declared legacy format ids when no canonical option-to-legacy mapping is available, so those buys can dispatch.
## 5.830.0 — September 30, 2026 at 6:41 PM UTC
* API guidance, connector confirmations, schemas, and localized next steps now use Apostra and Apostra Support consistently while preserving legal and compatibility identifiers.
* Account setup, marketplace, connections and operator guidance now use the Apostra name in every supported language. Legal Scope3 references and registered Scope3 provider identities are unchanged.
* Apostra Slack messages now use Apostra wording in account-access, App Home, and assistant greeting copy.
* In-chat widgets now use Apostra for platform, operator and support naming while preserving stable identifiers and customer-provided content.
* Adding a Seller Account now asks **How would you like to sell?** — **Just list** or **Create an AMC account** — and the new storefront starts with that choice. The answer decides which seller plans your organization is offered and which workspaces the account shows. You can still change it later in **Settings → How would you like to sell?**
* Your organization can now choose the payment terms Apostra asks sellers for: `net_15`, `net_30`, `net_45`, `net_60`, or `net_90`. Nothing changes unless you set them, and `net_30` applies until you do. Set `paymentTerms` with Update billing info (`PUT /api/v2/billing/info`); the choice covers every advertiser in your organization.
* When Apostra opens an account with a seller for you, such as when you turn a seller on for an advertiser or buy from a seller for the first time, the request asks for your terms. The seller accepts them by approving the account, or declines the account; sellers never change the terms. On storefronts Interchange hosts or manages, only `net_30` is approved automatically, so longer terms wait for the seller's review, and some sellers decline them because they carry the credit risk.
* Accounts a seller has already accepted or rejected keep their decision and terms; a new choice applies to accounts opened afterwards. For now, a seller that doesn't take requested terms gets the request again without them, so the account opens on that seller's default terms. See "Your organization's payment terms" in Buyer setup.
* Canonical VAST creatives pinned to an unconstrained product format can now be delivered to sellers that still use the older AdCP creative format. Exact seller mappings continue to take priority, and no action is needed on your side.
* In a Demo Storefront Library, after you select "Prepare synthetic teaching materials", the Library now says the sample documents are still processing and keeps checking until they are ready. It then shows "Open held-out synthetic buyer brief" without a reload. Previously it could stop at "0 proposed facts still need review." with no way forward. If processing takes longer than five minutes, the Library stops checking and offers "Retry synthetic evaluation". Nothing to change on your side, and your own documents are unaffected.
* Let seller delivery resolve legacy creative format refs instead of pinning migrated product options.
* Send property-list include filters to sales agents that declare discovery filtering support, and avoid sending unsupported exclusion filters during product discovery.
* Fix storefront media-buy updates that fail after a source has accepted them: the buyer update is kept for seller readback instead of being closed as safely rolled back.
* When a linked partner storefront leaves the marketplace, it cannot receive new briefs, media buys, budget increases, or new packages. Existing linked bookings continue to support delivery, cancellation, decreases, creative sync, and approvals while the partner storefront still exists.
* Pages such as Campaigns and the advertiser roster open faster in the Apostra web app: the page starts loading while its access check is still running, instead of waiting for the check to finish first.
* Account-scoped media buys now keep the account relationship that authorised them when they are sent to a seller. Delivery reads for that same account can find those buys; other accounts and customer-wide reads remain separate.
* Every new storefront now starts with a seller product, so seller plans are offered for Seller Accounts created outside supply-side signup. Accounts added with **Add account** or `POST /api/v2/accounts/create-child`, and storefronts created with `POST /api/v2/storefront`, start on **Just list** unless the request sends `operatingMode: "AGENTIC_MEDIA_COMPANY"`. Storefronts Apostra provisions for an Apostra-managed ad server start as Agentic Media Company. You can change the seller product at any time in **Settings → How would you like to sell?**
* Support controls, activity and guidance now use the Apostra Support name. Existing access permissions and customer controls are unchanged.
* Apostra's signing identity is now `https://api.apostra.com`. If your sales agent verifies the RFC 9421 signatures on our calls, you can configure it with `https://api.apostra.com`: its `/.well-known/brand.json` now lists that agent URL, pointing to the same request-signing key we already use. Verifiers set up with `https://api.interchange.io` keep working, because that URL stays listed with the same key. To have us sign calls to your agent, declare `request_signing` in your `get_adcp_capabilities`; the Authentication docs describe the steps.
## 5.829.0 — September 30, 2026 at 5:26 PM UTC
* Accepting an IU plan from Plan & Billing no longer fails with a generic error when the Rate Card includes an activity at no charge, such as Enhanced Reporting included on the September plans. The accepted plan shows that activity by its standard name and records it as included. If an earlier attempt failed, accept the plan again. Nothing else changes.
* The Advertisers Page filter row is easier to read and use. The Dimensions filter now shows how many values you have selected instead of listing each one, so it keeps a single fixed width and no longer pushes the search field aside. The search field is wider as a result. Each advertiser row's action — "Open", "Review 2 proposals", "Create first campaign" — is now a real button, so it can be reached and activated from the keyboard. Clicking anywhere on the row still opens the advertiser.
* Your agent phone line no longer answers a text that is more than 15 minutes old by the time the agent reaches it, so a sender never gets a late reply to a stale question. Blank texts and texts to a line whose storefront is archived, replaced, or in adapter mode are also left unanswered instead of being retried. A text from a number that has opted out never reaches the agent, even when the opt-out arrives while the text is waiting. The agent answers at most 5 texts per sender every 15 minutes and 60 per line every hour, so two automatic responders cannot keep a line texting indefinitely. A text the agent is answering when the service restarts is finished or safely set aside, and it is never answered twice.
* Buyers can read live delivery for their own campaigns on any storefront, including external AdCP storefronts without a seller Distribution profile. Buyer authorization and tenant scope are unchanged.
* When Creative Sessions cannot complete an image generation, Murph now receives a clear generation result instead of an unexplained internal error. Leave the engine blank to use the funded default unless you choose a specific provider. Nothing to change on your side.
* Selecting Branding & distribution from the seller setup rail now opens the page after publisher domains are added. Payment & billing and other setup pages are unchanged, and there is nothing to change on your side.
* Murph in Slack now keeps processing messages that arrive together while it refreshes its connection to your workspace.
* Media buys accepted by a seller are now saved before follow-on lifecycle evidence is recorded, so a local evidence-write failure cannot make an accepted seller buy look failed or disappear from Apostra.
* Creative reviews now reject products that are missing an exact format mapping for an older sales agent before approval. Approved creatives also show delivery failures separately from the approval decision, so sellers can refresh the product route and retry instead of seeing a pending or completed state.
## 5.828.1 — September 30, 2026 at 4:25 PM UTC
* Searching your Library now finds text in your documents that previously did not appear in results.
* Campaigns now loads a smaller page after an account switch by leaving unrelated widget translations out of its download.
* Embedded sales agent 1.49.2: bump urllib3 to 2.8.0 past new security advisories.
* The sign-in and sign-up pages now appear as soon as they load when you aren't signed in, instead of showing a loading spinner while the app checks for an existing session. If you are already signed in, you are still taken straight to your account, and nothing changes on your side.
## 5.828.0 — September 30, 2026 at 3:40 PM UTC
* Archive and restore seller Library documents from the Documents list.
* In Media Partners, a connection whose sign-in didn't finish now shows **Finish connecting**, so you can retry that same login without removing it first. Clicking **Add connection** again reuses the unfinished connection instead of adding another empty one, and the list shows at most one unfinished connection per media partner.
* Campaigns now loads advertiser currency and time-zone settings reliably after account switches, including for advertisers with older metadata. Nothing changes on your side.
* On the Media Partners page and in the Media Partners widget in chat, the confirmation dialogs (Remove connection, turning a media partner on or off, data sharing, direct billing) now open in the part of the page you can see, so Confirm and Cancel are always reachable. Before, with a connection expanded, the Remove dialog could open partly below the visible area and could not be scrolled into view, so the connection could not be removed. When the widget area is short, the dialog now scrolls on its own. Nothing to change on your side.
* When `save_advertiser` cannot link, unlink, or update an advertiser's partner account, it now returns the error code and the actual reason, such as the account not being found or its currency not matching the advertiser. Before, every failure came back as a generic "Account assignment failed.". Internal failures still return a generic message. Accounts on a connection you authorized, such as Spotify, Google Ad Manager, or Meta, are linked with `save_connection` and `advertiserMapping`, using the account `id` from `get(kind:"connection")`. The tool text and the not-found error both say so. Advertiser-level Seller activation (`save_connection` `advertiserActivation`) is in limited rollout, and the docs now say what its `ACCESS_DENIED` error means. Nothing to change on your side.
* For organizations with the new settings group, administrators can use **Account settings → Organization** to add, archive, and restore child accounts, manage organization members, and review billing and contract details. The account switcher lists switchable accounts and pending invitations.
* When Apostra rejects a Pinterest `create_media_buy` or `update_media_buy` before sending anything to Pinterest, the error now carries `details.mutation_outcome: "not_dispatched"`. Retrying such an update with the same idempotency key now runs it again instead of replaying the saved failure; a create retry does the same when the error is correctable (`recovery: "correctable"`). Errors after Pinterest received a change keep the key and replay as before. Nothing to change on your side.
* Embedded sales agent 1.49.1: prove GAM write permissions per tenant with a no-spend canary, bump oauthlib and pyjwt past new security advisories, declare brief and wholesale media buy buying modes.
* Connection save results now validate stable action, browser handoff, and mutation receipt fields while preserving provider-owned metadata.
## 5.827.2 — September 30, 2026 at 2:21 PM UTC
* The media partner connections widget now shows two cards side by side at narrower widths. Previously the grid dropped to a single column below 768px, which left a column of wasted space in narrower chat panels; it now keeps two columns down to 640px.
## 5.827.1 — September 30, 2026 at 1:37 PM UTC
* When Apostra rejects a Snap `create_media_buy` or `update_media_buy` before sending anything to Snap, the error now carries `details.mutation_outcome: "not_dispatched"`. Retrying such an update with the same idempotency key now runs it again instead of replaying the saved failure; a create retry does the same when the error is correctable (`recovery: "correctable"`). Errors after Snap received a change keep the key and replay as before; a create that Apostra deleted from Snap after a later step failed now reports `details.mutation_outcome: "rolled_back"`. Nothing to change on your side.
## 5.827.0 — September 30, 2026 at 1:06 PM UTC
* The reporting docs now describe the `views` metric correctly. `views` counts content views as the seller reports them: a video view such as 30 seconds or the midpoint, an audio stream start, or the view event set by the package's pricing model. It is the unit CPV pricing bills on. Earlier docs said `views` counted viewable impressions and could be divided by `impressions` to get a viewability rate. That was wrong. Don't use `views` to estimate viewability. The numbers themselves haven't changed, and there's nothing you need to change.
* Meta wholesale `get_products` now stamps `ext.scope3_execution_readiness` on every product when the buyer's Meta ad account is connected, not only the app, lead-messaging, catalog, Shop and Event products. Pixel-based products such as Sales, Website Leads and Landing Page Views now show at discovery time whether the account has a Pixel, instead of failing later at `create_media_buy`. Page candidates are now the Pages the selected ad account can promote, the same Pages a media buy accepts, so discovery no longer offers a Page that `create_media_buy` would refuse.
## 5.826.0 — September 30, 2026 at 12:03 PM UTC
* Fixed linked storefront source discovery so an active partner storefront stays connected while it responds to a buyer brief. Pilot storefronts can receive their partner's offers again without reconnecting or retrying.
## 5.824.0 — September 30, 2026 at 11:03 AM UTC
* Organization admins can manage members and membership settings from child accounts they administer. Member lists and settings now check direct admin access to the requested account or its parent organization. An organization admin in a child account can also accept the organization's billing terms after confirming them in Plan & Billing; the acceptance belongs to the parent organization.
## 5.823.0 — September 30, 2026 at 10:27 AM UTC
* Organization administrators can now ask their agent to create a Buyer Account under their organization, including from a text-only host. Nothing to change on your side; existing Accounts and calls behave as before.
* The agent first calls `review_buyer_child_account`, which is read-only and shows the name, Buyer role, remaining account capacity and inherited access. It then calls `request_buyer_child_account`, which returns `approval_required` with a link. Nothing is created until an administrator of the organization opens the link, signs in on our site and approves. That can be a different administrator from the one who asked. The request expires after 24 hours. Repeating it with the same `idempotencyKey` returns the outcome, including the new Account ID.
* Creation sends no invitations, grants no direct membership, changes no feature flags and creates no media buy. Organization administrators inherit access. The Account is not ready to transact until Buyer Setup is done.
## 5.822.0 — September 30, 2026 at 10:05 AM UTC
* The Library no longer asks you to review selling points; the agent cites your documents directly. It also no longer creates wholesale-product proposals from document prose.
* Pilot storefront owners' active linked storefront sources now receive buyer briefs and bookings. When a buyer's brief reaches your storefront, it is forwarded to the partner storefront, and the partner's offers come back unchanged, with no markup. Pass-through only: the buyer pays what you pay the partner, and you keep 0% on the platform. There is nothing to change on your side. Sources you already activated start routing as long as the partner's consent is still current.
* Storefronts outside the pilot see no change.
* Only active linked storefront sources with current partner consent receive new briefs and bookings.
* When one booking spans several linked partners, each partner receives only the packages chosen from its own offers.
* Pending, disabled, archived, and no-longer-authorised links get no new briefs or bookings. Existing bookings keep their recorded terms for reporting and cancellation.
* Active linked storefront sources now report `selectable: true`.
## 5.821.0 — September 30, 2026 at 9:07 AM UTC
* Your storefront now opens on your work, not on last quarter's numbers. Where a live storefront used to land on Seller analytics — RFP win rate, booked budget, response rates — it now opens the **operations home**: your active buys, ranked by what needs you, with up to three flags on each. A task waiting on you. Delivery that is not moving as the flight implies. The buyer's goal being missed. Money the flight will not deliver comes first — the budget minus what your current spend rate projects over the whole flight, not the hole so far — then work waiting on you, then a goal behind, then everything on track. Within the first group, buys are ordered by the share of their budget at risk rather than the raw amount, so a large number in a small currency does not outrank a buy that will miss half its budget. The Media buys entry in your rail opens in the same order: it is the same page over the same buys, and ordering them differently by how you arrived would rearrange the list with nothing to explain it. The three flags stay separate on purpose: "spend 200 a day at 80% viewable" is two answers, and one combined score would tell you to act without saying what to act on. Red is earned by a running flight and means the promise is not being kept; yellow is worth acting on; a verdict we cannot support yet reads "not enough data yet" in neutral and never in red. Seller analytics has not gone anywhere — it keeps its place in the Operate rail, one click below Media buys, and a saved `?view=dashboard` link still opens it directly. API callers get the same ranking with `sort=attention` on `GET /api/v2/storefront/media-buys`, and an `attention` block on every row wherever the list is read. Pass `activeOnly=true` to drop buys that are finished and cannot need anyone — the operations home does, because the list is newest-first and a buy that settled yesterday would otherwise take a slot from an older one still running. That ranking orders one hydrated page — its tiers come from delivery and goal verdicts that only exist once a page is read — so it is available only with `skip=0`; asking for it on a later page returns 400 rather than quietly serving urgency order, and the response echoes `attentionRanked` either way.
## 5.819.0 — September 30, 2026 at 7:07 AM UTC
* If your seller account is set to Just list, the left rail now shows the Inventory workspace alongside Buyers and Agents. Inventory is where you follow your sources, media buys, approvals and activity. Advertisers and the Merchandising tools stay part of an Agentic Media Company account, and so does connecting an ad server through an Apostra-managed sales agent. To use those, switch in Settings under How would you like to sell? Nothing to change on your side.
* Source diagnostics now show the request and response exchanged with your agent, including the complete A2A message, task status, and artifacts, instead of collapsing nested content into `[REDACTED_DEPTH]`. You can read the exact call we sent: its AdCP version, buying mode, account reference, and the buyer's brief, all of which your agent already received. Credentials, tokens, API keys, authorization headers, and personal data such as email addresses and phone numbers stay masked. A single value over 8,000 characters, or a body over 1 MB, is shortened and marked as truncated.
## 5.818.0 — September 30, 2026 at 6:45 AM UTC
* When your storefront agent answers a brief, it can now use passages from your processed, unarchived Library documents right away and cite the exact slide, page paragraph, or sheet range it used. Prices still come only from your rate card and products.
* Pilot storefront owners can now set pass-through terms on a pending linked storefront source and activate it. With pass-through, the price the buyer pays is exactly what you pay the partner, you keep 0% on the platform, and you settle with the partner outside the platform. Activation re-checks that the partner storefront still consents and needs terms in place first. If either check fails, the source stays pending. Nothing changes for your existing sources until you set terms and activate them.
* Active sources don't receive briefs or bookings yet. Routing to active linked storefront sources comes in a later release.
* Pass-through (`pass_through`) is the only pricing mode today. Other modes may come later.
* You can change terms after activation. Each change creates a new version.
* Every booking made through a source records the terms in force when it was booked. Changing terms or disabling the link later leaves existing bookings as they were.
* Who collects the buyer's payment (the platform, which then pays you, or you directly) comes from the buyer account's existing approved billing setting. It is recorded on each booking.
* Your rate card now appears in your Library as a document; editing prices saves a new version of it.
* The storefront media-buy list (`GET /api/v2/storefront/media-buys`) now carries delivery for every buy on the page, not only when you filter the list to one buyer relationship, and buys your ad server manages carry delivery at all for the first time — they previously always read `delivery: null`. Each row gains the counts a buyer's goal is judged on (`clicks`, `views`, `completedViews`, `conversions`, `leads`), each `null` when the source reported no value rather than a zero that would read as a total miss. `delivery.pace` measures spend against how much of the flight has run instead of against the budget alone, and returns `on_pace` (within 10% of the even-pace line), `behind` or `ahead`, or a named reason when there is no verdict. A new `reporting` block says whether delivery can be expected at all: `reported`, `awaiting_first_report`, or `blocked`, where `blocked` always carries a `detail` naming what is in the way — so an ad server that has not granted us reporting access reads as blocked instead of sitting on "awaiting first report" forever. A source that never answered the question reads `null` rather than claiming either. A buy your ad server manages reports impressions and spend together or not at all, and arrives in a **new `adServerDelivery` field** rather than in `delivery`, because an ad server states a running total without naming the day it runs through and `delivery` has always published a `lastDeliveryDate`. `delivery` therefore keeps its exact published shape, `lastDeliveryDate` included and still a required string, and source-managed buys read `delivery: null` there exactly as they always have. `views` is content views, the quantity CPV pricing bills on, and is never a count of viewable impressions. Delivery is an enrichment, never a reason the list fails: if delivery reporting cannot be read at all, every row's delivery, pace and goal read empty, a response `warning` says so, and the buys themselves are unaffected. `reporting` is `null` in that case rather than `awaiting_first_report` — the latter is a positive claim that nothing has been reported yet, and "we could not read it" is not that. One limit worth knowing: a routed buy from a **third-party AdCP buyer** — one with no account on this platform — reports no delivery, and so no pacing or goal progress either. Delivery is attributed by the buyer it was reported under together with the sources the buy was routed to, and a third-party buy carries no buyer identity that reaches reporting, so a total for it cannot be proven to belong to one buy rather than two. We would rather show nothing than a number that might be another buyer's. Buys your ad server manages are unaffected. Delivery is also attributed conservatively: a row whose reported buyer does not match the buy's is not shown on it, and a buy whose delivery was reported under more than one buyer shows none at all rather than a total belonging to nobody in particular.
* Nothing to change on your side: every field is new, and no existing field is renamed, removed, changed in type, or made optional. Read `adServerDelivery` when you want the new ad-server totals.
* You can now see the buyer's goal on every buy on your storefront, and whether delivery is hitting it. `GET /api/v2/storefront/media-buys` and the per-buy timeline (`/media-buys/{mediaBuyId}/timeline`) carry a `goal` block: what the buyer asked for, what your own terms committed to, what has actually been achieved, and a verdict of `on_track`, `behind` or `beat` — the same judgement the buyer sees, computed by the same code over the delivery your sources reported. When the evidence does not support a verdict, `verdictWithheld` says why rather than guessing, and a withheld verdict is never a bad one. You see the goal only from what the buyer told you: the commitment recorded against the buy at booking, and the `optimization_goals` on the AdCP request the buyer sent you. A buy whose goal was never disclosed to you carries `goal: null`, as does a buy whose packages, or whose forwarded sources, were asked for different things — the verdict is judged against the whole buy's delivery, so there is no single target to state and none is invented. For a buy your ad server manages, `optimizedBy` says who is acting on the goal — `ad_server` when its own optimizer works towards it, `seller` when nothing automatic does and your ad operations own it. `seller` is the row to watch: it means the goal is yours to hit. Nothing to change on your side; every field is new.
* Pages that make many requests at once load faster: the Terms of Service check now runs once for a burst of requests from your account instead of once per request. Accepting the Terms of Service still takes effect immediately.
## 5.817.0 — September 30, 2026 at 5:57 AM UTC
* Signed-in web app requests no longer look up your contract on every call before doing their work, so pages that make many requests at once load faster. Terms-of-service checks work exactly as before.
## 5.816.1 — September 30, 2026 at 5:26 AM UTC
* Organization admins can set up payouts and accept Terms from Plan & Billing in chat. Opening Plan & Billing from the organization, in Apostra, Claude, or ChatGPT, now saves the organization's payout entity. Every Seller Account that has no payout entity of its own is paid to it. Admins of a single Seller Account inside an organization still cannot change where its payouts go.
## 5.816.0 — September 30, 2026 at 4:59 AM UTC
* Campaign delivery grouped by `channel` now also reads the channels a product's formats declare. When a seller leaves a product's own channels empty but declares AdCP `applies_to_channels` on its formats, those rows show the channel instead of `null`. A format type on its own, such as video or audio, is still not treated as a channel. Nothing to change on your side.
* Organization admins can manage payouts for their Seller Accounts again. In Plan & Billing on the web, the Payouts tab at the organization now saves and shows payout entities for the organization and for each of its Seller Accounts. Payout control stays with organization admins: an admin of a single Seller Account inside an organization still cannot change where its payouts go.
## 5.815.0 — September 30, 2026 at 4:35 AM UTC
* Sellers can add a rate card as a typed Library document, preview parsed rows before they change live pricing facts, and cite the committed document row in quoted prices.
* Advertiser exclusion lists now reach sales agents that support exclusion-list targeting. Apostra continues to filter and block excluded inventory on the buyer's side.
* Your account's logo now appears as soon as the page opens, using the logo already saved for your storefront or advertiser, instead of showing initials while it's fetched from your brand's website.
## 5.814.1 — September 30, 2026 at 3:21 AM UTC
* Clarifies the [Frequency Caps guide](https://docs.scope3.com/v2/guides/frequency-caps) with a limitation that already existed: `targetingOverlay.frequency_cap` isn't enforced the same way on every publisher for shapes outside a simple per-day count. Meta silently accepts an out-of-shape cap (for example, a whole-flight cap) without applying it, instead of rejecting the request the way TikTok and Snap do; Snap also requires `per: "individuals"` and only honors a cap for a subset of optimization goals. Check the [ad platform compatibility guides](https://docs.scope3.com/v2/guides/ad-platform-compatibility/index) for the exact shape each platform's native cap supports before relying on this field as a guaranteed cap. No API behavior changed — this only corrects what the guide says about existing behavior.
## 5.812.0 — September 30, 2026 at 12:02 AM UTC
* Your agent phone line now replies to texts. The agent reads each one as a brief, composes against your catalogue, and texts back one short summary of what it drafted — nothing bought, approved, or changed, and no account data in the reply; whoever texted still signs in to act on it. Each text is answered once, on its own: the agent has no memory of earlier texts or replies, so a follow-up should restate what it needs. `STOP` opts a number out until it texts `START`, and the agent never sees a message from an opted-out number. Replies are on for Seller Accounts enrolled in agent phone lines; a line whose account is not enrolled keeps its texts queued unanswered until Apostra enrolls it.
* Advertisers on Meta, Pinterest, Reddit, and Snap no longer show **Failed** on the Sellers page. Each now shows the step it needs: **Connect account**, **Reconnect**, **Map account**, or **Active** once mapped. Two earlier releases said this was fixed, but it wasn't; this release completes the fix.
* Rebuild the buyer discounts widget — where a seller sets rate-card discounts for buyer brands and buying operators — on the Apostra design system, and change how you reach the add and delete steps.
* "Add discount" now sits in the widget's header. Choosing it opens a dedicated "New discount" screen instead of expanding a form under the list, so the list and the form are never on screen at once, and an "All discounts" control takes you back. Removing a discount asks for confirmation on its own screen in the same way, rather than in a dialog floating over the widget. Editing an existing discount is unchanged — it still opens in place, in the row.
* Every rule on the add form carries over. A duplicate brand-or-operator domain still sends you to the existing row's editor instead of letting you post a create that would fail, a percentage outside 0–100 still holds Save closed with a quiet hint, the coverage preview still tells you what a domain will and will not cover before you commit it, and a refused save still shows the reason and keeps your draft to retry.
* Choosing between a brand and an operator discount is now a real pair of radio buttons rather than a tile that shaded when picked. The domain search is the design system's search field, so its icon is sized to the field and it offers a clear button once you have typed. The header no longer repeats the total discount count and the per-section headings no longer repeat it either — the All / Brand / Operator tabs carry it. Each row's percentage sits one step down the type scale, so a row reads as a row rather than competing with the widget's own title.
* The widget card, tabs, form controls, chips, and alerts now match the rest of the converted in-chat fleet, which means they also inherit its focus rings, error styling, and keyboard behaviour. Loading and load-failure states render inside the card instead of flush on the chat background.
* Seller Setup now groups your go-live work by what you can do about it: **Needs setup** for the steps waiting on you, **Waiting** for the ones waiting on someone else, and **Done** for what is settled. Once your storefront is live those read as **Needs attention**, **Waiting** and **Running**.
* Every step appears in exactly one place. Inventory sources and publisher domains used to show up twice — once as a checklist row and again as their own section — and now each appears once, with its full detail inside the step it belongs to. A finished step keeps its detail and its action, so you can still add a second source or edit a policy without hunting for it.
* Confirming your settlement currency is now two labelled fields: the primary currency you are paid in, and an optional list of currencies buyers may pay with. Once confirmed, the step reports them on separate lines — "Primary: USD", "Additional: EUR" — rather than one comma-separated list.
## 5.811.0 — September 29, 2026 at 11:02 PM UTC
* Apostra now requests a wholesale catalog only from agents that declare `wholesale` in their buying modes, as AdCP specifies: an agent that declares its modes without `wholesale`, or declares none at all, is not asked for one. If your agent serves a wholesale catalog, make sure its capabilities list `wholesale`. Source diagnostics stop reporting those calls as failures, and the unhealthy status the earlier rejected calls left on your source clears on the next check. Live buyer briefs still reach your agent, and nothing about them changes. One exception: if your inventory source is configured in Apostra as selling wholesale, we keep requesting the catalog and the calls keep failing — tell us and we will correct the source's configuration to match what your agent declares.
* Accepting an invitation no longer shows "You do not have access to this customer" right after you join. The invitation page now waits while your new access becomes active (usually under a minute), then opens the account. If access still is not active after about a minute, the page says so and offers **Try again**, which reopens the account without needing a new invitation.
* Connected Library sources now explain safe sync failures on the source card, while the worker reports one diagnostic event per failed source pass.
* Demand Exchange responses now include revision-pinned Library passage citations for grounded response sentences.
* Your storefront dashboard now loads its analytics at the same time as the rest of the page instead of after it, so the dashboard fills in sooner when you open or reload your storefront.
* Require typed browser-handoff details when a V3 connection authorization is requested.
## 5.810.0 — September 29, 2026 at 9:13 PM UTC
* Proposal results now name official platform sellers such as Spotify, Meta, and TikTok by the platform's own name. Previously these sellers could appear under the name of the company operating the connection (for example "Scope3"), so you couldn't tell whose products you were looking at.
* Murph answers seller questions again, both in the app and in Slack. Some of those questions had been failing with "Murph could not complete the publisher authorization lookup". Murph now checks the publisher authorization docs only when you actually ask about publisher authorization or `adagents.json`, and every such answer is still based on that page. Nothing to change on your side. If a question failed earlier, ask it again.
* Exclude products whose stored website, app, or CTV property identifiers match an advertiser exclusion list.
* Embedded sales agent 1.48.0: send ESA application logs to Sentry Logs, gAM probe reports NO\_NETWORKS\_TO\_ACCESS as a connection failure.
## 5.809.2 — September 29, 2026 at 8:39 PM UTC
* Library source previews now include supported files in nested folders and Google Docs, Slides, and Drawings. They explain excluded files by type and only say when a count is limited when there are more files to check.
* `log_event` now accepts every `action_source` an event source can be registered with: `offline`, `chat` and `email` join `website`, `app`, `phone_call`, `in_store`, `system_generated` and `other`. If you registered an offline-sales, chat or email event source, you can now send its events without changing anything else.
* Widget header buttons now sit flush with the widget's right edge on surfaces that do not show a collapse control. Widgets previously held that corner open for the collapse chevron everywhere, so on a surface that shows no chevron the button looked like it floated inboard of the edge, out of line with the content below it.
## 5.809.0 — September 29, 2026 at 7:23 PM UTC
* Tidied the Murph navigation rail. Setup status glyphs (the rings beside each checklist row) are now drawn at the same size and weight as the icons beside them, and a part-finished step's progress arc sits on its ring instead of floating outside it. Hovering a row in the go-live checklist now highlights the same width as every other row, rather than pressing the fill flush against the label. The workspace tabs across the top of the rail have their spacing back — they had lost it to a vertical rule between each tab — and they now scroll sideways when they do not fit, so the last tab is reachable instead of clipped. Rows are wider: the rail's side padding was uneven (wider on the left than the right) because it reserved room for a scrollbar that most browsers draw as an overlay and never occupy; the gutters are even again and tighter, giving each row more room for its label. The collapse control has moved out of the rail header and now sits beside Settings in the footer on both the buyer and seller rails.
* The web app now keeps its code in your browser between visits, so reloading a page no longer re-checks each file with the server before it starts loading your data.
* Campaign delivery grouped by seller or sales agent now names the seller when a sponsoring seller reads a self-serve client's delivery, or when an app registration is scoped to one account. Before, those rows came back unattributed. Lifetime campaign delivery reads in those same scopes are now sized against the right account, so an oversized request is refused up front instead of running. Nothing to change on your side.
* Connected Google Drive, Microsoft 365, and Confluence Library sources now synchronize into Library materials and show a reconnect state when provider access is revoked.
* You can upload supported files from the Library again. Nothing to change on your side.
* Buyer agents can now group campaign delivery by media channel. Pass `channel` in `dimensions` on a V3 `get_delivery` call with `report: "campaign_delivery"`, and each row's `channel` lists the AdCP channels (such as `ctv`, `olv`, or `display`) declared by the product behind that package, so a seller that sells several channels is split by what each package bought. A product that declares several channels reports them as one list, so no delivery is counted twice; rows whose product declares no channels, or whose package has no product, return `null`, and the summary says how many. Channel comes from the product's current record, so a seller changing a product's channels re-files that product's past delivery. It does not affect targeting or billing, it is separate from your own `channel_group`, and the REST `/reporting/metrics` endpoint does not have it. Available now; nothing to change on your side.
* Each media buy in Get reporting metrics (`GET /api/v2/buyer/reporting/metrics`) now says whether its money is moving on schedule, separately from whether it is hitting its goal. `goalProgress.pace` compares spend with how much of the flight has run, not with the budget alone: a buy three days into a thirty-day flight that has spent a tenth of its budget is on pace, and a buy on its last day that has spent the same tenth is nine tenths undelivered. It returns the share of the flight elapsed, the share the reported spend covers, expected and actual spend, spend per day against budget per day, and a verdict of `on_pace` (within 10% of even pace), `behind` or `ahead`. Both sides of that comparison always cover the same stretch of flight: narrow `days` and expected spend narrows with it, so a week of spend is never measured against a month of expectation. When the evidence is thin, `verdict` is null and `verdictWithheld` names the reason: no budget, no flight dates, the flight has not started, the window covers none of the flight, the window covers under 5% of the flight, or no reported spend. Campaign-level `goalProgress` carries no pace, and the V3 `get_delivery` tool does not return it. Rate goals are now judged on what they divide by rather than on their own count. A rate needs at least 1,000 impressions before it gets a verdict, so a low click rate over millions of impressions now reads `behind` where it was withheld, and a rate over fewer than 1,000 impressions is withheld even when its count is large. `viewable_rate` goals are now recognised, and read `metric_not_reported` instead of `metric_unsupported` until sellers' viewable and measurable impression counts reach reporting. `views` is content views and is never used to estimate viewability. Nothing to change on your side: pace and `actual.denominatorUnits` are new fields, and no existing field is renamed or removed.
* The customer Slack app now appears as Apostra. You can use `/apostra` in Slack, and the existing `/murph` command continues to work.
* Show sellers which Library documents are still needed to run their storefront agent, sell guaranteed inventory, or promote a catalogue.
* Source Diagnostics now shows when products from your source were excluded from anonymous buyer discovery requests because their own properties matched an exclusion list. It reports only your affected product IDs, names, and matching domains.
## 5.808.0 — September 29, 2026 at 4:37 PM UTC
* Status alerts now stretch to fill the panel they appear in, so a short message and a long one line up on the same left and right edges instead of shrinking to fit their text.
* Agent text lines now protect you from runaway sending. Each line sends at most 10 texts a minute and queues the rest; it texts only ordinary North American (+1) numbers, never premium-rate numbers or short codes; and it stops sending for the rest of the UTC day once that day's texts reach the line's daily spend limit. A text to a destination the line cannot reach is refused immediately with `400`, so nothing is queued.
* The unsupported `filters.geo_countries` and `filters.excluded_content_categories` fields now return a clear correction before any storefront is contacted. Existing requests using standard, negotiated, or vendor-specific AdCP filters continue to work without changes.
## 5.807.0 — September 29, 2026 at 3:27 PM UTC
* Media-buy submission now waits until the seller has accepted the exact creative revision before sending a library reference. If creative sync is still processing, the buy remains unsubmitted and can be retried after the seller finishes.
## 5.806.0 — September 29, 2026 at 3:03 PM UTC
* Storefronts in the pilot can now add another storefront on the platform as a linked storefront source, as long as that storefront has an active seller account relationship that consents to it. The new source starts pending: it can't receive briefs or bookings yet, and activation with pass-through terms comes in a later release. If you're not in the pilot, there's nothing to change on your side.
* Create one with the existing Source create API or MCP tool.
* You can disable a linked storefront source at any time, including after pilot access is turned off.
* Source reads return a new optional `linkedStorefrontTarget` field. It describes the partner storefront and why the source isn't active yet. `linkedStorefront` is unchanged and still returns null, so existing calls behave the same.
* Archived linked storefront sources keep their history but no longer appear in public source reads.
## 5.805.0 — September 29, 2026 at 2:45 PM UTC
* Approved creatives that needed a retry now reach the sales agent. Since 28 September, a retried creative sync on a media buy could stall without contacting the sales agent until its delivery deadline passed. Retries now go through as normal, and the media buy's session timeline records each sync once.
* When you generate creative variants in chat, the gallery keeps completed image previews even when the session details are too large to return in full. Nothing to change on your side.
## 5.804.1 — September 29, 2026 at 2:01 PM UTC
* Murph now honours a buyer's requested multi-storefront discovery wait, including long waits and explicit wait seconds, when it continues the same product query.
## 5.803.1 — September 29, 2026 at 1:40 PM UTC
* Allow a new buyer discovery to replace an unfinished discovery when no runtime owns it.
* Advertisers on Meta, Pinterest, Reddit, and Snap that still showed **Failed** on the Sellers page now show the step they need: **Connect account**, **Reconnect**, **Map account**, or **Active** once mapped. The earlier fix did not reach every one of these platforms.
## 5.802.0 — September 29, 2026 at 12:45 PM UTC
* You can now call every Apostra V3 tool over plain HTTP, with no MCP client required. Each V3 operation that exists as an MCP tool is also a fixed route, `POST /api/v3/tools/`, described by a public OpenAPI 3.1 document at [https://api.interchange.io/api/v3/openapi-3.1.yaml](https://api.interchange.io/api/v3/openapi-3.1.yaml). The REST routes and the MCP tools are generated from one operation registry, so they accept the same inputs and return the same results. Authentication is the same as for V3 MCP: an API key or OAuth.
* Every write request needs an `Idempotency-Key` header, so retries are safe:
* Retry a valid request with the same key and the same body, and you get the original result back. Results replay for 7 days after the write settles.
* Reuse a key with a changed body and the route returns `409 Conflict`.
* If the write is still running, or its outcome is uncertain, the route returns `202 Accepted` with `Retry-After`. Keep polling with the same key. A new key starts a new write.
* Nothing changes for existing V3 MCP connections or v2 REST integrations. TypeScript and Python SDKs are coming separately and are not part of this release.
* The suggested prompts on Murph's opening screen load faster, because the account checks behind them now run at the same time instead of one after another.
* When you connect a Library source, returning from provider sign-in now continues setup automatically when possible. If the browser cannot open the sign-in tab or finish the handoff, the Library shows a clear way to continue or check again.
* Discovery now shows when an advertiser's exclusion list removed seller products, including the total, a count for each seller, and bounded product and matching-domain details. Empty results caused by an exclusion list explain what was removed instead of appearing as no matching inventory.
* The web app now reuses the browser's saved cross-origin permission checks, so pages you revisit within two hours skip an extra round trip before each API request.
* Agents on the V3 tools can now read what a seller committed to for your goal on each booked media buy. `get` and `search` for a media buy, and a campaign read with its media buys, return the same `goalCommitment` the V2 buyer reads already carry: the commitment kind, the goal you asked, the target the seller's terms answer, and one answer per line. `get` lists up to 50 line answers and `search` and the campaign read up to 20 per buy, and each says when it cuts. Buys booked before commitments were recorded are unchanged.
## 5.801.2 — September 29, 2026 at 11:46 AM UTC
* Continue buyer discovery requests by their execution handle while preserving the original search arguments.
* Opening a link to another account you belong to now loads that account directly. The web app no longer loads your previous account first and then switches, so these links open about two server round trips sooner.
## 5.801.1 — September 29, 2026 at 11:16 AM UTC
* Approved no-spend source tests can now complete while Storefront intake remains paused. The Storefront stays paused, and ordinary buyer discovery remains closed.
## 5.800.0 — September 29, 2026 at 10:47 AM UTC
* Keep products from sellers that already answered when a get\_products wait budget expires during page assembly.
* An `update_media_buy` sent straight to Meta with a stale `revision` now fails with `CONFLICT` and `details.mutation_outcome: not_dispatched` instead of `SERVICE_UNAVAILABLE`, and nothing in the request is applied on Meta. Read the media buy again with `get_media_buys` and resubmit with its current `revision`; updates held for seller approval still fail with `SERVICE_UNAVAILABLE`.
* Murph now preserves a later product read or unrelated answer when an earlier product discovery is still incomplete. Discovery and attachment safeguards continue to prevent unconfirmed results from being presented as complete or attached.
* Show product-discovery results as they arrive when a product search is still running.
## 5.799.7 — September 29, 2026 at 10:26 AM UTC
* The campaign creative upload task no longer shows the generic "An internal error occurred" banner when a save or readback step fails after the creative was actually saved — it now shows a specific message about what to check, with a support reference number when one is available.
## 5.799.6 — September 29, 2026 at 9:54 AM UTC
* Embedded sales agent 1.47.0: honor or reject every package optimization goal, optimize GAM CPM line items toward CPC, CPCV and viewable-rate goals, seller-managed elsewhere, prove GAM read permissions per tenant up front, accept Talpa FreeWheel CPM readback, optimize live buys' goals and target-less goals.
## 5.799.3 — September 29, 2026 at 6:28 AM UTC
* `request_proposals` now enforces its no-country refusal for every buyer. Set `targetingOverlay.geo_countries` on the campaign, then retry with the new campaign revision. This requirement was announced for 12 October 2026 and has been enforced since 29 September 2026.
## 5.799.2 — September 29, 2026 at 6:17 AM UTC
* Enrolled buyers who request five image directions now receive all five when they generate a saved creative draft. Other creative generation requests are unchanged, and no action is needed.
## 5.799.0 — September 29, 2026 at 5:28 AM UTC
* Buyers enrolled in Creative Engines can compare generated variants in chat, explicitly refine one direction, and save a selected output to their Creative Library.
## 5.798.0 — September 29, 2026 at 4:47 AM UTC
* Video and audio creatives now report play progress through VAST tracking events.
* Sellers can find Library documents faster with one control row, clear document statuses, editable categories, and bulk category updates.
* Storefront admins can save publisher domains from the Property Roster again. Click **Save domains** after entering domains; no setup or API changes are required.
## 5.797.0 — September 29, 2026 at 4:28 AM UTC
* The web app makes fewer round trips when you come back to a page you loaded in the last two hours, because your browser now remembers the API's cross-origin permission check instead of repeating it before every request.
* Platforms you connect with your own account, such as Meta, Pinterest, Reddit, and Snap, no longer show your advertisers as **Failed** on the Sellers page. These platforms bill your connected account directly, so connecting the account is your billing agreement. Each advertiser now shows the step it actually needs: **Connect account**, **Reconnect**, **Map account**, or **Active** once it is mapped.
* The credit-application endpoints (`/api/v2/buyer/credit-applications/v2`) are retired: every call now returns `410 Gone` with a `GONE` error code and `Deprecation` and `Sunset` headers, and the endpoints are removed after 27 November 2026. Apostra does not grant buyer credit lines: sellers decide credit on their own terms, and buying from sellers on consolidated billing needs only your billing details. If your integration calls these endpoints, remove those calls before that date. Existing applications remain on record.
* `GET /auth/me` now returns `customerAccess`, which says how the session reached the account it describes: its own account, an account the user belongs to, or Apostra staff or support access. Use it to tell a membership switch from staff access, since the account appears in the returned account list either way.
## 5.796.2 — September 29, 2026 at 2:41 AM UTC
* The chat composer and the bar above it now use the Apostra design system, so the place you type matches the rest of the conversation around it. Two fixes come with it: the `+` button always opens its menu instead of sometimes jumping straight to the file picker, and people in a shared room are each shown in their own colour again. Everything the composer does is unchanged — attachments, slash commands, mentions, queueing a message while an answer streams, and stopping a reply all behave exactly as before.
* Media buys that never reach a seller because dispatch retries are exhausted now surface a clear "Failed" status and reason instead of staying stuck in Draft indefinitely with no explanation.
## 5.796.1 — September 29, 2026 at 1:27 AM UTC
* Progressive product discovery no longer loses progress when saving it briefly stalls. Seller results that arrive during a slow save are kept and appear on your next poll or `discovery.revision` event, instead of staying stale until discovery finished. When several sellers answer at almost the same moment, one `discovery.revision` event now carries all of their groups, so revision numbers can skip.
## 5.796.0 — September 29, 2026 at 12:07 AM UTC
* Creative Engines image drafts can request 1 to 8 distinct variants with `request.variant_count`. Preview requests first check capacity for the planned set, then reserve each direction before dispatch; concurrent use can return fewer directions than requested.
* V3 adds `targetingOverlay` as the campaign targeting field. `targeting` is deprecated with behavior unchanged. Its removal is a separately announced change (AI-10440) with at least 14 days' notice.
## 5.795.0 — September 28, 2026 at 10:41 PM UTC
* Creative image sessions now save supported aspect ratios as canonical dimensions and return terminal generation failures without polling indefinitely.
## 5.794.0 — September 28, 2026 at 10:16 PM UTC
* TypeScript write methods now require a caller-supplied `idempotencyKey`; Python write methods require `idempotency_key`. The SDK sends this value as the V3 `Idempotency-Key` header, never creates or replaces it, and treats an HTTP 202 as an in-flight result rather than a completed write. Update existing write calls to retain and reuse the same key when recovery is appropriate.
## 5.792.0 — September 28, 2026 at 9:23 PM UTC
* Fixed a bug where a buyer's previously selected account could incorrectly carry over into a fresh sign-in on their Claude connector, instead of returning to their usual account.
## 5.791.0 — September 28, 2026 at 9:04 PM UTC
* The account switcher now shows a standalone account under its own organization header, with the account listed beneath it — the same way multi-account organizations already appear. Previously a standalone account rendered as a flat, always-open list with no organization context, so switching into it never showed which organization you were in.
## 5.790.0 — September 28, 2026 at 8:19 PM UTC
* Seller operators can now find retained standalone media-buy Sessions in their default Session search, without selecting a source first.
## 5.789.0 — September 28, 2026 at 7:07 PM UTC
* Sellers with an active Demo Storefront can review its deterministic synthetic campaigns through the existing V3 delivery query. Responses identify the fixture and are unsuitable for commercial decisions; expired or revoked demos do not return synthetic data. Public REST remains held behind the AI-10305 launch gate. AI-10126 retains Demo Storefront attachment and lifecycle completion, including its daily canary.
## 5.788.1 — September 28, 2026 at 6:48 PM UTC
* `get_media_buy_delivery` now reports what your sources actually reported. Delivery totals are summed from each source's own media-buy row instead of the response-wide `aggregated_totals`, which AdCP has deprecated: a source that omitted that field used to contribute nothing to your totals, and a source whose aggregate covered more buys than the one you asked about used to contribute too much. A source that returns delivery for several buys but not the one asked about is now treated as a failed read instead of silently attributing another buy's figures. And the response now tells you which of your packages delivered — every source's per-package figures are relayed under your own package ids, where the response previously always returned an empty list. Every package line carries its spend, pricing model, rate and currency, so package spend stays readable even when the buy's combined total is withheld because sources disagree on currency. Other package fields are passed on when they are valid AdCP; a field AdCP does not accept is left off and named in `errors` as `PACKAGE_FIELDS_OMITTED`. A package that cannot be described completely, for example because its source gave no currency or no price, is left out of `by_package` and named in `errors` as `PACKAGE_DELIVERY_INCOMPLETE` with the reason, instead of being reported with guessed values. `get_media_buys` now takes a media buy's `currency` and `total_budget` together: when the source's live answer is missing either one, both come from the stored buy record, so a budget is never labelled with a currency from a different snapshot.
## 5.788.0 — September 28, 2026 at 6:38 PM UTC
* When Meta cannot confirm whether a media-buy update, pause, or cancel was applied, and cannot undo it, the request now fails with a clear "outcome unknown" error instead of a generic provider error. The error is `SERVICE_UNAVAILABLE` with `recovery: terminal`, `details.reason: "adapter_update_outcome_unknown"`, `details.mutation_outcome: "unknown"`, and `details.retry_safe: false`. Do not resend that change as-is: check the media buy's current state first, then submit any further change as a new request.
* Apostra no longer automatically re-sends a media-buy update, pause or cancel after an attempt whose outcome it could not confirm. Each new pause or cancel you start is sent as its own request. Updates that wait for seller approval, Amazon changes, media-buy creation, and requests sent straight to a storefront's MCP endpoint are unchanged. Nothing to change on your side.
## 5.786.0 — September 28, 2026 at 4:57 PM UTC
* Archive and restore seller Library documents through the API while preserving their source history and keeping archived documents out of new composition.
* The storefront workspace loads its inventory sources and Teach health checks at the same time as the storefront itself instead of afterwards, so the dashboard and left menu fill in sooner.
## 5.785.0 — September 28, 2026 at 4:28 PM UTC
* Add an operator-run historical media-buy Session import.
## 5.784.0 — September 28, 2026 at 3:59 PM UTC
* Media buys now stay failed when an inventory source reports a terminal booking failure, instead of being shown as not submitted and retried automatically. Temporary network and service failures remain retryable.
## 5.782.0 — September 28, 2026 at 3:23 PM UTC
* Buyers using an AI assistant connected to Interchange can switch accounts again after their session lands on their organization's top-level account. Previously, switching to one of the organization's buyer accounts from there was refused.
* You no longer need a card on file or an Apostra credit line to buy from sellers on consolidated billing. Apostra takes no credit risk on the seller's media, so the only requirement for those buys is your organization's billing details: a payer name, street address, and country, so Apostra can invoice you. Add them in **Plan & Billing → Payment & invoices**. If you already have a card or an Apostra credit line on file, you can keep buying while you add them. Until they are complete, a buy from a consolidated-billing seller is refused with the new `billing_details` blocker, buyer readiness lists the new `BILLING_DETAILS` value and reports `commercial.billingDetailsComplete: false`, and the buyer setup checklist shows an **Add billing details** step. Buyer readiness no longer reports `PAYMENT_AUTHORITY` or the `payment_authority` blocker; both stay in the schema so older clients still parse. If your integration reads buyer readiness, treat any blocker value it does not recognize as blocking. Sellers that bill you directly are unaffected.
* Storefront reporting that finishes processing before an upload returns now keeps the buyer's delivery summary up to date.
* Delivery now tells you whether a campaign is achieving its goal, not just how the budget is pacing. Every media buy block and campaign block in buyer reporting, plus the campaign report in `get_delivery`, carries a `goalProgress` field. It holds the goal you asked for, the target you set and the cost the seller committed to, the achieved cost per unit or rate computed from the same delivery the block reports, and a verdict of `on_track`, `behind`, or `beat`. The verdict is withheld, with the reason, whenever the evidence does not support one: no goal or target was set, the metric is not one delivery can score yet (event goals other than leads, return on ad spend, and metrics such as reach wait for event-scoped counts), the seller has not reported that metric yet, or there are not enough observations yet. Each read also says the number is the seller's own count, how fresh it is, and whether the seller has signalled that a later report will supersede it. A buy booked at 3.00 per click that delivered 500 clicks for 2,000 in spend shows an actual of 4.00 per click and a `behind` verdict; the same buy on day one, with only two clicks logged, shows the actual cost per click while the verdict stays withheld because two observations are not enough.
* Plan & Billing no longer shows an Apostra credit limit or an "Apply for credit" option. Apostra does not grant buyer credit lines: sellers decide credit on their own terms, and buying from sellers on consolidated billing needs only your billing details. Your funded balance, amounts owed, invoices, and payment methods are unchanged.
* When you stage a V3 media buy, `save_media_buy` now accepts the complete AdCP package targeting overlay (language, device, daypart, and demographic targeting), not the partial set it accepted before. Package targeting on those dimensions must now stay within your campaign's own targeting; a package that falls outside it is rejected with a `CONFLICT` error naming both the campaign's and the package's values. Deprecated. Numeric codes replace names. Removal will be announced in release notes at least 14 days ahead; planned for 2 November 2026 (AI 10440). Codes must be in the Nielsen DMA dictionary.
## 5.780.0 — September 28, 2026 at 1:37 PM UTC
* Creative Engines now guides eligible buyers through the short path from a saved image draft to a usable generated image URL, with provider connections reserved for BYOK and voice work.
* Show media-buy updates and creative syncs in seller Session timelines.
* The Merchandise section of the seller navigation now follows the order you work in: Library first, then Inventory components and Signals, with Sessions last. The same rows are there and they open the same places; only their order has changed.
## 5.779.0 — September 28, 2026 at 12:49 PM UTC
* Organization administrators can now open **Plan & Billing → Accounts** to add child Buyer or Seller Accounts, review their status, usage, team access, and organization terms, preview archive blockers, archive an account, and restore it later. Archive preserves account history and is refused while an account has unfinished campaigns or executable or unsettled media buys.
* In a V3 organization session, `search` and `get` can read child accounts and members, including inherited Terms of Service and contract history. `save_account` can change only an existing account's name or company label; it cannot provision accounts, change members, archive, restore, or hard delete. Existing calls keep working, so no action is required.
* Return adapter-local media-buy refusals as correctable request errors instead of retried upstream outages.
* You can now save image Creative Session drafts when your agent uses common image format shapes. Invalid requests identify `request.format_kind` and show the canonical format fields to provide.
## 5.778.0 — September 28, 2026 at 12:20 PM UTC
* Cancelling a storefront buy while the seller is approving it now cancels it reliably. Previously, if the seller's approval landed at the same moment, the cancellation could fail with "already approved" and leave the approval in place, even though nothing had been sent to the seller yet.
* Dimension names and values now show as plain text in list filters and label pickers. Before, the Library's category and topic filters, and the label filters and label editor on the advertiser and campaign lists, could show internal markup around each name, for example ` Category `. Your dimensions and labels were not changed, and there is nothing to change on your side.
* In a Creative Session, you can now promote one approved generated output into an advertiser-owned Creative Asset in the Library. Each promotion keeps an immutable revision and an approval receipt. It also records the Creative Engine, the provider, and whether the output was generated on your own provider account or the Scope3-funded preview. That record comes from the generation call that produced the output, so outputs generated before this release can still be promoted but carry no provider record. Reuse rights follow the provider's terms, and Interchange does not verify them. Check those terms before you use the asset outside Interchange. Promotion does not finalise a Creative or export the output.
* To retry a promotion, send the same idempotency key to get the same receipt. If the session revision, output digest, or advertiser changes, use a new key.
## 5.777.0 — September 28, 2026 at 11:48 AM UTC
* Fixed campaign launch so a seller's pending confirmation cannot replace the returned revision or make the campaign appear to return to draft before a buyer can pause or cancel it.
* Canceled media buys now report a completed operational status when the seller has confirmed the cancellation. Statuses that need attention include a reason.
* In Sessions, you can open a linked brief or media-buy timeline from its chat event. The action appears when the event includes that item’s reference and respects your existing access permissions.
* `sync_accounts` on a storefront Interchange hosts or manages now rejects a request that sets `payment_terms` with `PAYMENT_TERMS_NOT_SUPPORTED`, instead of creating the account on the seller's own terms without saying so. Omit `payment_terms` to accept the seller's default terms. Storefronts that route accounts to the seller's own sales agent still forward `payment_terms` to that agent unchanged.
* The web app no longer retries a request the server has refused (for example, a page you don't have access to). The page shows that result right away instead of after several seconds of repeated attempts.
* Catalog V3 results now validate stable identity, lifecycle, and health fields while preserving provider-owned feed details.
* Seller V3 save results now validate stable media-kit and inventory-source fields while keeping provider metadata extensible.
## 5.776.1 — September 28, 2026 at 11:02 AM UTC
* Treat an empty media-buy Session advertiser ID list as no advertiser filter.
## 5.776.0 — September 28, 2026 at 10:49 AM UTC
* Scope3-funded preview image drafts now start with three distinct composition directions, while customer-key image engines keep one direction unless a request supplies its own axis. Enrolled Buyer Accounts can make up to 50 image requests per UTC day.
* Meta products no longer require a bid price except for the one product where Meta itself requires a value goal instead. `create_media_buys` and `add_discovery_products` previously rejected every Meta product without a positive `bidPrice`, even though Meta's own delivery options let advertisers omit a bid cap in favor of automatic, highest-volume delivery for every objective except website value optimization. Buyers can now add and execute Awareness, Traffic, Engagement, Leads, App Promotion, and plain Conversions products without supplying a bid; Meta delivers them at its default automatic bid strategy, matching what Ads Manager already allows. Website value optimization (Maximize Website Conversion Value) is unchanged and still requires a ROAS floor or maximize-value goal instead of a bid.
* Build scheduled jobs and backend integrations against 53 account-aware Apostra operations through fixed v3 HTTPS routes or a generated OpenAPI 3.1 client. Authenticate with an existing API key or M2M application; existing MCP and v2 integrations keep working without changes.
* Embedded sales agent 1.46.1: classify FreeWheel profile source metadata.
## 5.775.0 — September 28, 2026 at 10:20 AM UTC
* Show recorded storefront media-buy exchanges in seller Session timelines.
* Add the seller Sessions workspace, retained-session filters, and session document handoffs. The Sessions view no longer includes a private-notes composer; existing notes remain read-only timeline events.
## 5.774.0 — September 28, 2026 at 10:10 AM UTC
* You can now select countries in the advertiser setup form, and the country menu closes when you click elsewhere. This changes only the form interaction; advertiser country matching and API behavior are unchanged. Nothing to change on your side.
* Sellers whose country or channel coverage is not yet declared can remain available for selection, with that coverage marked unconfirmed. An official adapter's registered channel limits still apply. A Critical Supply approval still selects a seller by default only in its approved markets and channels. When a seller responds to product discovery with an error and no products, the response now reports a seller failure if no cached catalog is available, instead of treating the error as an empty catalog.
* Make Session searches require an advertiser to match both advertiser filters when callers provide a singular advertiser ID and an advertiser ID list.
## 5.773.0 — September 28, 2026 at 9:15 AM UTC
* Approved test inventory on a paused Storefront now appears in that source's preview, test plan, and confirmed no-spend test. It remains hidden from buyers and disappears from tests when its approval is revoked or expires. No action is needed.
* The assistant appears as Apostra, with Apostra branding and links, when the product-brand cutover is enabled. Existing Murph links and integrations continue to work without customer action.
## 5.772.0 — September 28, 2026 at 8:34 AM UTC
* Meta campaign-level budget buys now launch instead of being rolled back after creation.
## 5.771.0 — September 28, 2026 at 8:18 AM UTC
* V3 media buys and the campaign defaults attached to proposals now respect geography set on the campaign through the v2 API as well as V3. Before, a V3 media buy could ignore v2-set campaign geography.
## 5.770.0 — September 28, 2026 at 7:28 AM UTC
* Meta campaign-level budgets now work when products carry a bid price.
* Every media buy now records what the seller committed to for your goal when it was booked. `goalCommitment` on the media buy carries the commitment kind (`guaranteed`, `best_effort`, `report_only`), the goal you asked, the cost the seller's terms commit or aim at, whether every product meets your asked cost, one answer per product, and the proposal version it came from when you accepted a proposal. Buys created without a proposal get a commitment from their own pricing and delivery terms. Buys booked before this stay without one.
## 5.769.0 — September 28, 2026 at 7:01 AM UTC
* Publish the V3 webhook signing, retry, secret-rotation and deduplication contract so developers can prepare a receiver before endpoint registration becomes available.
## 5.768.1 — September 28, 2026 at 5:54 AM UTC
* Seller billing labels now say **Consolidated billing** and **Direct billing** instead of "Interchange-cleared" and "Seller-cleared", matching the billing documentation. Under consolidated billing you send your invoice to Apostra, which bills the buyer and pays you once the buyer's payment arrives; you keep the buyer's credit risk, as with direct billing. Nothing changes in how billing works.
* Meta media buys that use a campaign-level budget now launch with the campaign's bid strategy.
## 5.768.0 — September 28, 2026 at 5:22 AM UTC
* a campaign-level budget buy on Meta can now include several products from the same Meta storefront and channel group; Meta distributes the one campaign budget across their ad sets, and per-product budgets stay refused.
## 5.767.0 — September 28, 2026 at 4:48 AM UTC
* Eligible Creative Engines preview buyers can generate images and video with Gemini, OpenAI, fal.ai/FLUX, or Veo paid by Scope3. The preview is limited to 25 generation requests per account and 500 globally each UTC day, with Veo also limited to 3 requests per account and 50 globally each UTC day. Voice providers and customer-key connections remain unchanged.
## 5.766.2 — September 28, 2026 at 4:38 AM UTC
* A media buy whose targeting doesn't overlap its campaign's targeting now returns a conflict error naming both values, instead of silently targeting everywhere. A buy can only narrow its campaign's targeting. For V3 campaigns, the guardrail currently covers campaign geography; it will extend to every campaign overlay field when V3 campaigns move to `CampaignTargetingOverlay`. V2 language and device constraints continue to be applied at launch.
* Meta Sales Campaign and Maximize Website Conversion Value media buys now default to Facebook and Instagram Feed, Stories, and Reels placements, instead of silently narrowing to Feed only when a buyer's brief doesn't name placements explicitly.
## 5.766.1 — September 28, 2026 at 4:11 AM UTC
* Meta media buys that use a campaign-level budget now launch correctly.
* Storefront navigation now loads in its final shape instead of adding or moving workspace tabs after the page first appears.
## 5.766.0 — September 28, 2026 at 3:35 AM UTC
* Speed up repeat Page opens by reusing unchanged widget shells, correctly identify the built-in host during widget data loading, and measure when Library first becomes usable.
## 5.765.0 — September 28, 2026 at 3:10 AM UTC
* Creative Engines generation results now return each completed image's URL, MIME type, dimensions, and preview. Saved drafts include the next generation call, and pending work includes an exact poll call.
* Creating a media buy that hit a transient seller error during a platform deployment could permanently stop retrying, even though the same request would normally succeed on the next attempt. Retries interrupted by our own worker restarting are no longer counted against the attempt limit, so a deploy landing mid-retry no longer causes a media buy creation to fail for good.
* When a Meta campaign brief mentions an audience by name in free text instead of the structured audience field, Meta now resolves it against your live custom-audience list and applies it, or asks you to clarify if the name is ambiguous, unresolved, or conflicting. Previously the audience mention was silently dropped with no indication it had been ignored.
* V3 campaigns that target only a region such as `"US-CA"`, a Nielsen DMA, or a postal area now reach the sellers that cover that country when you call `request_proposals`; before, such a campaign went to every seller with no country filter. Starting 12 October 2026, `request_proposals` will also refuse a campaign whose targeting names no country, before any seller is contacted. Set at least one country with `save_campaign` `targeting.geo` (for example `"US"`) to keep proposal rounds running.
## 5.764.0 — September 28, 2026 at 2:49 AM UTC
* New Meta media buys can now spend from your campaign's saved budget total instead of fixed per-package amounts, with Meta distributing spend across packages automatically.
* Set `budgetAllocation.mode` to `seller_optimized` on `save_media_buy` (or `create_media_buys` on v2) when you create a Meta media buy from explicit product selections. Meta then allocates your campaign's saved budget total and currency across the buy's packages — the Advantage+ campaign budget behavior you'd see in Ads Manager. Per-product budgets aren't accepted with `seller_optimized`; Meta distributes the campaign's saved total. The total is snapshotted onto the media buy when it's created, so a later change to the campaign's budget doesn't affect a buy that already exists. Use `mode: "fixed"` with a positive budget on every selected package, or omit the field to keep today's behavior, where a package without a budget is funded from the campaign's remaining budget. No enrollment is needed.
* This only applies to a new Meta media buy built from explicit product selections on a single Meta storefront, and to one such buy per campaign per batch. It isn't available for proposal-based buys, carts that mix in other storefronts, storefronts that require seller approval, campaigns without a saved budget total, or existing media buys — you can't add it after the fact. Buys that omit `budgetAllocation` are unaffected.
* Meta audience targeting (`targetingOverlay.audience_include` / `audience_exclude`) no longer rejects a Custom Audience that was shared into your ad account from a data partner such as LiveRamp — a common agency workflow that previously failed every time. A genuinely invalid or non-deliverable audience reference now returns a clear, non-retryable error naming the problem, instead of a generic "Upstream request failed" that looked like a temporary outage and got retried pointlessly.
## 5.763.1 — September 28, 2026 at 1:58 AM UTC
* Our billing documentation now describes consolidated billing accurately. When a seller uses consolidated billing (billing party `agent`), the seller sends its invoice for a buy to Apostra, Apostra puts it on the buyer's consolidated media bill and collects, and pays the seller once the buyer's payment arrives. Apostra acts as a billing agent: it does not extend credit for the seller's media, does not pay a seller before the buyer pays, and is not liable if a buyer does not pay. Sellers keep the buyer's credit risk, exactly as with direct billing. Nothing changes in how billing works today, and there is nothing to do on your side.
## 5.762.0 — September 28, 2026 at 1:13 AM UTC
* `add_discovery_products` now warns when a submitted selection collapses into an existing one instead of failing silently. Give each repeat of the same product a distinct `selectionId` (e.g. several Meta ad sets on the same product with different budgets or audiences) to keep it as its own line item. Submitting repeats without a `selectionId` in a single call still merges them into one package for backward compatibility, but the response now names the collapsed product in `warnings` instead of dropping the other submissions silently.
## 5.761.0 — September 28, 2026 at 12:23 AM UTC
* Session timelines now show recorded RFP briefs and proposal terms in readable text. Sellers can filter Session activity by agent or person, advertiser, and buyer account without matching text in a brief or conversation.
## 5.760.0 — September 27, 2026 at 11:27 PM UTC
* When your certified Agent is ready to serve other organizations, Agent details now points to the next Partner action. The private-offer and payment process is unchanged.
## 5.759.0 — September 27, 2026 at 11:00 PM UTC
* Session search now names the future media-buy history source and adds Agent or Human classification plus exact advertiser and buyer-account filter fields. The new source returns no history and the new filters remain unavailable until their source adapters support them.
## 5.758.0 — September 27, 2026 at 10:24 PM UTC
* Measurement sources can now name their measurement vendor the way sellers and optimization goals identify it. Add `vendor` (the domain that hosts the vendor's brand.json, such as `placer.ai`) and `vendorMetrics` (the vendor's metric ids, each mapped to one of the source's outcome types) when you create or update a measurement source. Both fields are optional and existing sources are unchanged. This is the first step toward letting a campaign goal be counted by a vendor such as Placer.
## 5.757.0 — September 27, 2026 at 10:03 PM UTC
* Agents using the Set Up a Campaign skill (version 1.5.0) now ask how you will know a campaign worked before saving a goal. They then propose a one-line goal statement for you to confirm: the goal, the target, who counts the result, and the commitment you want from sellers. Once proposals come back, they compare sellers on those terms: who guarantees a price, who will only aim for it, and who only reports. The goal-seeking campaigns guide now also explains what changes when you choose not to set a goal.
* When you sign in, an old Scope3 Seller membership no longer overrides your Apostra Buyer account or verified Storefront. Nothing to change on your side.
## 5.756.8 — September 27, 2026 at 9:41 PM UTC
* Long account names now truncate in the customer selector instead of overlapping the role badge and menu chevron.
## 5.756.7 — September 27, 2026 at 9:21 PM UTC
* Make the Agent Overview easier to scan by consolidating diagnostics into one details row and shortening the certification action and source-attention list.
* Fixed adding and executing a Meta Sales campaign optimized for purchase value with a ROAS floor: a buyer who declared the ROAS floor goal at the campaign level (`update_campaign`/`create_campaign`'s `performanceConfig.optimizationGoals`) rather than on the individual product was still rejected — both at `add_discovery_products` and again at `execute_campaign` — with "provide optimization\_goals ... instead of bid\_price", because those checks only recognized a goal declared on the product itself. They now also recognize a satisfying goal declared at the campaign level, matching how the goal is actually dispatched to the seller.
## 5.756.4 — September 27, 2026 at 4:09 PM UTC
* Storefront cancellations now keep media-buy commitments in place until the seller confirms a terminal outcome, exposing unconfirmed cancellations as ending rather than releasing budget early.
* `create_media_buys` and `save_media_buy` now support adding the same product more than once in one call — for example several ad sets on the same product with different budgets or audiences. Give each repeat a distinct `selectionId` (v2 `selections[]`) or `products[].selectionId` (v3) to keep it as its own media-buy line item. Previously, repeating a product with no `selectionId` silently collapsed every repeat into a single package, keeping only the last submitted budget with no error or warning. Submitting repeats without a `selectionId` still merges them into one package for backward compatibility, but the response now names the collapsed product in `warnings` instead of dropping the other submissions silently.
## 5.756.1 — September 27, 2026 at 2:19 PM UTC
* `create_campaign` now scopes the product discovery it runs automatically to the storefronts you pinned with `storefrontIds`. Previously that inline discovery ignored the pin and could surface products from unrelated sellers; calling `discover_products` directly afterward already worked correctly and is unaffected.
## 5.756.0 — September 27, 2026 at 2:05 PM UTC
* When you request proposals for a campaign with an inclusion property list, the response now shows which requested properties sellers covered and why any properties are missing. Sellers that do not support property-list filtering are identified, so you can choose a supporting seller or revise the constraint.
## 5.755.0 — September 27, 2026 at 1:35 PM UTC
* Buyers can declare viewability, IVT, completion-rate, brand-safety, and attention-score standards per media-buy package, with readback and a clear launch blocker when seller support is not declared.
## 5.754.0 — September 27, 2026 at 12:46 PM UTC
* In V3 `save_campaign`, you can set `frequencyCap` with `level: "mediaBuy"` while a campaign is a draft. The AdCP 3.2 cap applies to each eligible seller media buy and is shared across that buy's participating packages. Proposal discovery returns only sellers and products that support the cap. A seller proposal must confirm the exact cap to be a usable quote, and launch checks seller and product support again before dispatch.
* `save_media_buy` also accepts an independent AdCP package `targetingOverlay.frequency_cap`. Completed synchronous and asynchronous creates must confirm a MediaBuy cap before V3 reports it as effective. Meta and Pinterest reject capped packages with multiple offerings before provider mutation, because separate ad sets or ad groups cannot share one package counter.
* Each seller media buy has its own counter. A cross-seller Campaign cap and live cap changes are not available. A bound proposal keeps its quoted cap; changing it requires removing the bound MediaBuy and requesting a new proposal. Existing calls and campaigns are unchanged.
## 5.753.2 — September 27, 2026 at 12:35 PM UTC
* Simplify the Agent Overview so its next step and key status facts are easier to scan, while keeping production, certification, and reporting evidence in details when needed.
## 5.753.0 — September 27, 2026 at 11:16 AM UTC
* The Agent test page now tells governed administrators when the claimed owner must run a test, while keeping certification-check status separate from test results.
* An `optimization_goals` / `optimizationGoals` request with the wrong field casing (for example `eventSources` where `event_sources` is expected) now fails with a clear message naming the correct field names, instead of a generic validation error. Tool descriptions for `add_discovery_products`, `update_campaign`, and `update_media_buy` also now state each field's expected casing explicitly.
* Long account names now truncate in the customer selector instead of overlapping the role badge and menu chevron.
## 5.752.0 — September 27, 2026 at 10:05 AM UTC
* Library document filters now search and page across the complete filtered result.
* Canonical storefront reloads now preload the landing module and report the complete landing timeline, matching account-switch navigation.
## 5.751.0 — September 27, 2026 at 9:45 AM UTC
* Campaign filters now search the full campaign collection and page against the filtered total.
## 5.750.2 — September 27, 2026 at 8:29 AM UTC
* Keep failed capability-probe backoff bookkeeping from invalidating current source configuration attestations.
## 5.750.1 — September 27, 2026 at 7:34 AM UTC
* Complete-key seller account operations now use current organisation, advertiser, and actor authority without a credential-scoped fallback. Public mapping guidance now documents country scope, deferred reauthorisation, and staged cutover safety.
## 5.750.0 — September 27, 2026 at 7:24 AM UTC
* Buyer activity, activity log, setup, Creative Engines, and Library now page long lists and show errors at the top.
* Eligible sellers can select recorded RFP exchanges in Sessions to read admitted history and open an exact linked Proposal Pass. Existing seller access checks still apply, and this does not enable RFP edits or private notes.
* Make Agent test setup status clear, including safe next steps when a test has not started, and keep test evidence distinct from certification.
* If Apostra has enabled agent phone lines for your Seller Account and assigned a text number, you can send text messages through the API. A `202` response means the message was queued, not delivered; retry the same message with the same request ID to avoid a duplicate.
* Switching between Storefront and Buyer accounts now opens the account you selected instead of returning to the previous one.
* Agent owners can request certification review without selecting an Inventory Source. The Agent test view now keeps the next action clear while a confirmation is pending.
* Discovery and proposal requests now show a safe outcome for every seller and the property domains represented in returned products. Sellers with no inventory for a request are shown as no-inventory results rather than failures.
* Account switches and signed-in page loads now check independent access settings together, reducing the time spent waiting for account details before the page can appear.
* Buyers can save campaign target and suppress audience lists and a catalog reference through V3. Campaign readiness now explains that these fields cannot be delivered to sellers yet, so campaigns using them cannot launch.
* You can now invite a specific contact to claim self-service access to a managed Advertiser. The Advertiser roster tracks email delivery, recipient claim, and your admission decision as separate states so you can see exactly what happened before acting.
* Submitted seller media-buy creates now settle through background task polling when a webhook is unavailable. Seller rejections are visible and terminal. If the seller does not return terminal evidence before the recovery deadline, the buy moves to a visible reconciliation state while its budget remains committed; a seller response or operator follow-up resolves it.
* Complete AccountKey seller-account feeds now export and import explicit dedicated and shared-default binding modes without changing native-account ownership.
## 5.749.0 — September 26, 2026 at 10:37 PM UTC
* Buyers can cancel a campaign and all of its media buys in one call. Cancellation is asynchronous and remains visible until every seller confirms it.
## 5.748.0 — September 26, 2026 at 9:22 PM UTC
* Buyers can bind an advertiser catalog to a campaign and read it back. Catalog-bound campaigns remain blocked from launch until seller catalog sync is available.
## 5.746.3 — September 26, 2026 at 8:07 PM UTC
* Two buyers on the same storefront can now each have a media buy with the same id routed to the same source. Before, the second buyer's media buy could fail to be recorded because the first buyer already held that id there.
## 5.746.2 — September 26, 2026 at 7:21 PM UTC
* Start with [Apostra V3 documentation](https://docs.apostra.com/v3/overview). Choose Manage ads for work in the application or a connected assistant, or Build software for the developer installation guide. Codex and Claude Code use the `apostra-developer` plugin; regular Claude and Cowork use the hosted MCP connector.
* Existing V2 integrations continue to work. No migration or credential changes are required.
* Seller accounts now require verified Seller setup before they can sign in or use product APIs. An old internal account classification or empty storefront no longer grants product access. Existing MCP credentials retain only account status and account-switching tools so operators can leave a blocked account.
## 5.746.1 — September 26, 2026 at 6:55 PM UTC
* Live Seller storefronts now start loading their dashboard from the account's existing liveness check instead of waiting for the detailed setup checklist to finish loading.
## 5.746.0 — September 26, 2026 at 6:39 PM UTC
* Storefront navigation now appears once with the correct workspaces instead of adding tabs while the page loads.
* Sessions now lets you open the exact Proposal Pass for a linked RFP request or response. Representation links remain read-only records, and access still follows the existing seller permissions.
## 5.745.0 — September 26, 2026 at 6:18 PM UTC
* Account Admins can submit a separate no-spend Source for certification fixture review after attesting the current observed endpoint. The submission is tenant-bound, replay-safe, and requires current isolation evidence. A reviewed sandbox Source is registered and parked for the existing bounded certification runner; neither submission nor review certifies the Agent, starts a run, or authorizes spend.
* When you save a draft Creative, the response now includes the revision required to update it. Nothing to change in existing calls.
## 5.744.2 — September 26, 2026 at 6:02 PM UTC
* Storefront pages now reuse the existing liveness check for the navigation alert instead of running a second full readiness check while the page loads.
* Anyone with read access to an advertiser can now find, attach, and deliver that advertiser's Library assets. The original uploader remains recorded for audit.
## 5.743.0 — September 26, 2026 at 4:37 PM UTC
* In MCP Session search, sellers can now select the Storefront RFP source, find retained RFP interactions by exact recorded buyer or advertiser labels, and read their structured history. These labels are discovery metadata and do not grant account access.
## 5.742.3 — September 26, 2026 at 3:52 PM UTC
* Seller account approvals now belong to the verified buyer organisation, so authorised colleagues share one account and approval while each action retains its own audit identity.
## 5.742.1 — September 26, 2026 at 2:21 PM UTC
* Seller storefront homes now reuse one readiness result when choosing the initial Setup or Dashboard view, avoiding a duplicate load during page startup. Nothing changes on your side.
* Seller dashboards now reuse storefront data that is already loaded, reducing the delay before dashboard content appears. No action is required.
## 5.742.0 — September 26, 2026 at 2:07 PM UTC
* When you switch between Storefront and Buyer accounts in Interchange, the account you selected now stays active instead of switching back. No action is needed.
## 5.741.0 — September 26, 2026 at 1:29 PM UTC
* Partner Program organizations with an issued private offer can now review and accept its exact terms in Plan & Billing. When the accepted payment choice supports CARD, they can continue from a compatible MCP app to secure card setup. Card details are entered only on the hosted payment page. Payment standing, ownership, registration, certification, and operating authority remain separate requirements.
## 5.740.1 — September 26, 2026 at 1:15 PM UTC
* Plan & Billing now reports the IUs your organization used instead of an internal provider cost. Accounts without IU usage see an IU-specific empty state. Nothing to change on your side.
## 5.740.0 — September 26, 2026 at 1:08 PM UTC
* Media buys now clearly show when their only creative is rejected before delivery.
* Campaigns, media buys, and connections pages now page long lists and show errors at the top.
* Media-buy errors from managed ad-server inventory now explain which targeting field or product setting needs attention instead of returning a generic invalid-request message. Update the named setting and resubmit the media buy; provider, authentication, and transport details remain hidden.
## 5.739.0 — September 26, 2026 at 12:33 PM UTC
* Buyer agents can create, update, archive, check, find, and read property lists through the V3 MCP surface. A campaign can apply one include list at a time through `save_campaign.propertyListId`, or clear that targeting with `null`.
* Eligible organizations without a selected IU plan now see the current activity schedule in Plan & Billing instead of a retired preview. Accepted plans keep the schedule they agreed to. Nothing to change on your side.
* Plan & Billing now uses Apostra consistently across billing screens, next steps, help links, and commercial offer PDFs. Existing activity and action codes remain unchanged. Nothing to change on your side.
## 5.738.0 — September 26, 2026 at 11:25 AM UTC
* Opening an advertiser from the Advertisers page works again in the web app.
* Meta can now deliver one selected image creative to each selected package when a buy has multiple package assignments.
* The Advertisers page now pages its list and shows any error at the top.
## 5.737.0 — September 26, 2026 at 10:41 AM UTC
* Meta ad accounts that contain campaign-budget (Advantage campaign budget) or daily-budget campaigns now read and sync. Before this fix, one such campaign made the whole account's media-buy read fail, which stopped status updates and directed-campaign syncs for every buy on the account. A bidding policy recorded once for the whole media buy is reported once in `ext.scope3_meta_bidding` instead of as each package's `bid_price`. A provider-authored campaign-budget bid cap without that recorded policy has no package `bid_price`, so it is not misrepresented as package-owned. Each campaign now reports what Meta actually holds. A lifetime campaign budget is reported as the total, and its packages carry no package budget. A daily campaign budget has no lifetime total, so the total is 0 and the daily amount is in `ext.scope3_meta_budget`; daily per-ad-set spend limits under a daily campaign budget are not reported. For a campaign that mixes lifetime and daily ad sets, the total counts only the lifetime ad sets, and each daily ad set carries no budget and states its daily amount in its `ext.scope3_meta_budget`. The update-budget action is no longer offered for campaign-budget, daily-budget, or mixed campaigns, because a per-package budget change cannot be applied to them. Per-package `bid_price` and `optimization_goals` changes on a campaign-budget buy are refused with a clear error, and nothing changes on Meta. Buys whose ad sets each hold a positive lifetime budget read exactly as before. Three shapes that already read are corrected: a campaign-budget campaign whose ad sets report a lifetime budget of 0 now reports the campaign budget as its total instead of 0, and a directed campaign mirroring it moves from 0 to that budget; an ad set that reports a lifetime budget of 0 alongside a daily budget now reads as a daily budget; and a campaign-budget campaign with no ad sets yet reports its campaign budget and no longer offers update-budget, which always failed for it. Some campaigns still cannot be read truthfully and still fail the whole account read: a campaign reporting both a campaign budget and ad-set budgets, or both a lifetime and a daily campaign budget, and campaigns with unsupported optimization or bidding settings. Nothing to change on your side.
* V3 MCP integrations can now read billing readiness, current Terms and the next setup action in text-only chat. Read `get` with `kind: "billing"`. When `nextAction.canCompleteInChat` is `true`, obtain the person's authorization for the returned Terms version, use the existing `save_billing` Terms operation and read again to verify the result. Accepting Terms requires direct organization-admin authority. Parent-container sessions can read readiness but keep their existing write restriction.
* Existing `save_billing` request, confirm, status and Terms behavior is unchanged. Use the new billing read's `paymentAuthority.verified` for the current payment method and `capture` or `submission` for setup history. The billing read cannot start secure card setup: `paymentAuthority.secureHandoff.available` is `false`. A versioned one-time handoff with fresh cardholder confirmation is still needed. See the [V3 billing flow](https://docs.interchange.io/v2/setup/v3/tool-reference#plan-and-billing).
## 5.736.0 — September 26, 2026 at 10:16 AM UTC
* Make synthetic demo reporting deterministic, internally consistent, and clearly labelled as unsuitable for commercial decisions.
* Make Property Roster publisher management admin-only and show non-admin members a read-only roster. Show a clear admin-required state in Plan & Billing instead of a generic load failure.
## 5.735.1 — September 26, 2026 at 9:01 AM UTC
* Approved creatives now reach third-party sales agents when the storefront and source use different identifiers for the same format. Previously affected deliveries retry automatically, and any remaining failure appears in the seller media-buy view with a clear owner and next action.
## 5.734.0 — September 26, 2026 at 4:15 AM UTC
* You can now upload WAV and MP3 files, up to 50 MB each, to the Creative Library for any advertiser in your buyer account, from Upload under More in the Library or by asking the assistant to upload the file. There's nothing to turn on. Each file is checked to confirm it's a valid WAV or MP3; once it passes, the upload confirmation shows it and it appears in the Library's Assets tab. Uploading saves the file but doesn't create or deliver a Creative; to build an audio Creative from an uploaded file, ask the assistant, since the Library's Add creative flow doesn't place audio files. Image and MP4 uploads work as before.
## 5.733.1 — September 26, 2026 at 3:38 AM UTC
* The Library page's Upload now accepts WAV and MP3 wherever audio upload is on.
## 5.733.0 — September 26, 2026 at 2:57 AM UTC
* Simplify the default Source overview to show a connection outcome and one supported next action, with connection evidence and technical diagnostics available on request. Provider-authorized Sources now prefill their saved endpoint separately from replacement credentials and only allow publisher endpoint edits that the pinned provider contract permits.
## 5.732.2 — September 26, 2026 at 1:42 AM UTC
* A seller's own category dimension is never taken over by the Library's system category.
## 5.732.1 — September 26, 2026 at 1:29 AM UTC
* Uploaded MP4 creatives now get their delivery renditions and poster frame produced automatically after inspection, so the Creative Library card, upload receipt, and preview show the poster without any further action.
## 5.732.0 — September 26, 2026 at 1:11 AM UTC
* An All countries advertiser's country scope no longer blocks matching with an eligible media partner. The Media Partners page labels the advertiser scope as All countries and keeps each media partner's own markets visible. Nothing to change on your side.
* After an upload finishes, the message the Task sends on your behalf is one plain sentence with a link to the Creative Library; the raw receipt stays inside the Task under its untrusted-receipt disclosure.
## 5.731.0 — September 25, 2026 at 11:58 PM UTC
* In staging, you can now upload WAV and MP3 files, up to 50 MB each, to the Creative Library for any advertiser in your buyer account, either from **Add creative** in the Library or by asking the assistant to upload a creative. There's nothing to turn on, and staging accounts that were previously told audio wasn't enabled can upload now. Each file is checked to confirm it's a valid WAV or MP3; once it passes, the upload confirmation shows it and it appears in the Library's Assets tab. To build an audio Creative from an uploaded file, ask the assistant; the Library's own creator doesn't place audio files yet. Uploading saves the file but doesn't create or deliver a Creative, and image and MP4 uploads work as before. Production rollout follows the validated staging check.
* If you can edit campaigns, select **New campaign** on the Campaigns Page to create an untitled draft and open it straight away. On a Page that isn't scoped to an advertiser, you choose the advertiser first. On the campaign, edit its name, budget (in the advertiser's currency), and flight dates directly; tracked campaigns stay read-only. In chat, ask for a campaign and the agent creates the draft and opens it. For MCP integrations, `save_campaign` now creates a draft without flight dates or a budget. Calls that already send them behave as before, and the REST create endpoint still requires both.
* If your storefront has the Library, you can now filter your Library documents by Category, Topic, Audience, and Vertical in the Library Page, and an Uncategorized chip finds documents that have no Category. These four are filled in and kept current each day from each document's existing category and relevance details, so there is nothing to set up; they don't count toward your limit of 20 dimensions, and if you already created a dimension with one of those names, yours is left unchanged. You can also create your own dimensions for Material with `save_dimension`, label documents with `save_material`, and filter by up to 20 dimensions at once (as chips in the Library Page, or with `filter.labels` when your agent searches Material); labels organize your Library and don't change inventory selection.
## 5.730.2 — September 25, 2026 at 9:04 PM UTC
* Product emails now arrive from Apostra with Apostra branding. Sustainability emails use the Scope3 by Apostra identity.
* Storefront discovery and merchandising simulations now continue when an early policy check claims a restriction that the seller's active policy does not contain. Explicit policy prohibitions still stop the brief, and sellers can distinguish an unsupported check from a genuine policy decline in the demand inbox.
## 5.730.1 — September 25, 2026 at 8:48 PM UTC
* Storefront media buys with declared creative formats now reach inventory sources without forwarding the buyer's reporting webhook. Failed bookings retain their status, reason, payload, budget, and currency for follow-up.
## 5.730.0 — September 25, 2026 at 8:31 PM UTC
* Storefront home now starts loading earlier for buyers and sellers, while keeping account checks in place. Seller setup status also loads sooner, and new timings show where any remaining wait occurs.
## 5.729.0 — September 25, 2026 at 8:08 PM UTC
* Creative delivery now keeps seller feedback with the current submission. A local delivery failure or later Creative revision no longer shows an earlier seller rejection as current.
* Meta feed image creatives now accept JPEG and PNG assets reported with their standard MIME types or file extensions. Unsupported image formats remain blocked. Nothing changes in how you create or attach a creative.
* We've turned off the automatic event source syncing to seller ad accounts (for example Snap or Meta pixel registration) that we introduced recently. The `sellerProvisioning` field no longer appears on event source reads, and buys no longer try to register your event sources on seller accounts automatically. We found structural problems with that first approach and are rebuilding it properly; your registered event sources themselves are unaffected.
* Media-buy reads now leave package and total budgets unset when a provider allocates from a shared campaign budget, rather than presenting stored line-item budgets as provider budgets. Nothing to change on your side.
## 5.727.1 — September 25, 2026 at 6:36 PM UTC
* Clarify whether a Source connection check is current, stale, or needs setup, and show one next action with its owner.
## 5.727.0 — September 25, 2026 at 6:26 PM UTC
* Storefronts now reuse the authenticated session response when loading the current account, removing a redundant startup request while preserving account and Terms of Service checks.
## 5.726.3 — September 25, 2026 at 6:11 PM UTC
* Seller dashboards now fetch recent RFP counts and rows at the same time, reducing the wait without changing the results shown.
## 5.726.0 — September 25, 2026 at 5:13 PM UTC
* MCP sign-in now keeps the browser confirmation page open until you continue to the local client. If the client is not listening, you can retry the callback from that page for up to 10 minutes without signing in again.
* Storefront account switches now reuse the account data returned by the switch itself, avoiding a duplicate loading request before the destination account appears.
## 5.725.1 — September 25, 2026 at 4:34 PM UTC
* Explain when Source Diagnostics requires a directly signed-in organisation admin to create an owned Sales Agent instead of offering an action that cannot succeed.
* Preserve each proposed product's sales agent and inventory source when a buyer saves a media buy from a text-only MCP client.
## 5.725.0 — September 25, 2026 at 4:04 PM UTC
* Embedded sales agent 1.46.0: certify FreeWheel booking per tenant with a no-spend canary, fail closed for unsupported FreeWheel targeting.
## 5.724.4 — September 25, 2026 at 3:25 PM UTC
* FreeWheel source setup now shows commercial booking readiness separately from read access, with the exact schedule timezone correction when a booking check fails.
## 5.724.2 — September 25, 2026 at 2:44 PM UTC
* Opening a storefront home now keeps one loading state while the account route resolves, including entry through the root URL. Nothing changes on your side.
## 5.724.0 — September 25, 2026 at 2:13 PM UTC
* Agent connection setup now opens after one bounded contract-history request and loads older published versions only when requested.
## 5.723.1 — September 25, 2026 at 1:26 PM UTC
* Clarification: the Meta AdCP 3.2 age-update capability announced in v5.716.0 is a substrate-only change today. No `save_media_buy` request path currently reaches this code because the three public AdCP 3.2 gates (`STOREFRONT_ADCP_SUPPORTED_VERSIONS`, `STABLE_ADCP_WIRE_VERSION`, `SUPPORTED_ADCP_RELEASES`) remain dark. Sellers running Meta will observe the described `targeting_overlay.demographics.age` update behavior once those gates activate in a later release; today's requests still land on the AdCP 3.1 contract and reject `demographics` on update as before. The analogous Snap and Pinterest slices (#16614, #16676) shipped as `type: internal` for exactly this reason; this note aligns the record.
* Media-buy create, update, and cancellation responses now identify inventory-source setup errors as terminal seller configuration problems instead of temporary outages. Buyers should ask the seller to correct the source configuration; retrying the same request will not help.
## 5.723.0 — September 25, 2026 at 1:01 PM UTC
* Seller storefronts now open in one stable state, with Chats collapsed until you open it. Nothing to change on your side.
## 5.721.0 — September 25, 2026 at 11:40 AM UTC
* Restore Activity as an always-visible buyer and seller left-rail destination.
## 5.718.0 — September 25, 2026 at 9:29 AM UTC
* Source account refreshes now handle valid task response wrappers and preserve safe diagnostics when an upstream account listing fails.
* Embedded sales agent 1.45.4: classify deterministic FreeWheel client errors, defer FreeWheel booking until creatives verify, reconcile FreeWheel inventory before trafficking, resolve FreeWheel schedule timezones.
* Every quoted proposal now says what the seller committed to for your goal. Each allocation carries a goal answer read off the seller's terms (its pricing option, the product's delivery type and measurement terms, and any optimisation goal it declared), with the cost the seller's terms commit or aim at and a commitment kind: `guaranteed` when a fixed price on the outcome comes with guaranteed delivery or a makegood policy, `best_effort` when the seller named a target or priced the outcome without a guarantee, and `report_only` when nothing in the terms is tied to your goal. The proposal-level `commitment` is the weakest kind across allocations and says whether every answer meets your asked cost. Older proposals are read the same way from their stored terms.
## 5.717.5 — September 25, 2026 at 8:51 AM UTC
* Conductor pull-request watch recovery now uses a repository-scoped GitHub webhook ledger index, preventing broad retained-ledger scans from delaying the repair lane.
## 5.717.4 — September 25, 2026 at 8:36 AM UTC
* Permanent creative approval callback failures now produce one immediate alert per destination and status, followed by a cooldown. Every failed callback remains recorded and available for support to inspect and replay.
## 5.717.3 — September 25, 2026 at 8:05 AM UTC
* `add_discovery_products` now accepts Meta's "Sales — Maximize Website Conversion Value" product without a placeholder `bidPrice`, as long as you declare a ROAS floor (`target: { kind: "per_ad_spend", strength: "floor" }`) or a `target: { kind: "maximize_value" }` optimization goal — this product never accepts a bid, since Meta only runs it as automatic delivery driven by that goal. Previously you had to add the product with a throwaway bid and then null it out via `update_campaign` before executing. The same check runs again at execution time, so removing the goal after adding the product is caught with a clear error instead of failing deep inside Meta's own bidding rules.
* Forward explicit insertion-order and placement timezone contracts when provisioning or rotating a managed FreeWheel sales agent connection.
* The Library's Documents list is easier to scan: the All documents / Recent rail and the pages-or-slides filter are gone, counts read as plain document counts, a document without a preview shows that in its thumbnail, and the table view drops the repeated type-and-date line under each name.
## 5.717.1 — September 25, 2026 at 6:43 AM UTC
* Creative approval callbacks that receive a permanent HTTP rejection now stop retrying automatically. The failed callback remains recorded for support to inspect and repair without flooding the buyer endpoint or creating repeated alerts.
## 5.717.0 — September 25, 2026 at 6:02 AM UTC
* Sessions can keep the selected RFP turn as removable private context for the existing Murph composer while you review that exchange.
* Video uploads now show an inspected poster frame in the Creative Library, upload receipt, and preview when it is ready. For a video Creative, the image in the thumbnail slot is used wherever the Creative shows a still; otherwise it uses the inspected poster frame. The upload receipt shows the poster once it is ready. Sellers receive the poster only where their destination has a thumbnail slot.
## 5.716.0 — September 25, 2026 at 5:29 AM UTC
* Meta media-buy updates on AdCP 3.2 now accept `targeting_overlay.demographics.age`, closing the last gap in the AI-5309 demographics slice for Meta. Before this change, a buyer who created a Meta media buy with a specific age range on 3.2 could not adjust that age range through `update_media_buy` — the request returned `400 does not yet support targeting_overlay.demographics`. Age changes on 3.1 continue to be rejected as before, matching the create-side gate. `min_spend_target`, canonical `bidding`, and canonical `budget_allocation` on 3.2 update remain deliberately blocked (see `update-media-buy.ts:880-893`) until they can be delivered with atomic-rollback semantics.
## 5.715.2 — September 25, 2026 at 3:43 AM UTC
* Improved storefront availability during temporary routing delays: affected storefronts now stay safely unavailable until routing is confirmed, then refresh automatically.
## 5.715.1 — September 25, 2026 at 3:27 AM UTC
* Sessions now shows its RFP ledger inside the Sessions page, keeping RFPs and conversation records separate without a second page heading.
* Optimization goals with a `cost_per` or `per_ad_spend` target now accept an optional `strength` field (`target`, `cap`, or `floor`). Some sellers only support a subset of strengths for a given target — Meta, for example, requires `strength: "cap"` for `cost_per` and `strength: "floor"` for `per_ad_spend`, and previously rejected every such goal outright because there was no way to say which one you meant. Set `strength` explicitly when targeting a seller with this constraint; omitting it keeps today's behavior unchanged.
* Sessions now opens native RFP exchanges in a persistent browser while keeping authorised Murph conversations separate.
## 5.715.0 — September 25, 2026 at 3:10 AM UTC
* You can now attach creatives from the campaign itself: on the Campaigns Page, open a campaign and select **Attach creatives** in its Creatives section, tick creatives from the advertiser's Creative Library, and select **Save creatives**. Unticking a creative detaches it when you save. Attached creatives run on every current and future media buy in the campaign whose products accept their format, and they're listed under **Attached**, separate from **Placed on media buys**. Drafts, reference material, uploads with no linked file, and creatives without a canonical format can't be selected, and each row says why. A creative live on an active or paused media buy can't be detached until that buy ends, and the picker manages up to 200 creatives per campaign. It's available now to anyone who can edit the campaign; buyers who can only view see no change, and tracked campaigns stay read-only.
* Creative review retries now reuse existing submissions without failing when the same creative content is submitted again.
* Meta product descriptions returned by discovery could state an open-ended age range (for example, "18 and older") that disagreed with the exact age range actually applied to the campaign. Descriptions now always include the resolved age range, so buyers reading product details see the same age targeting that gets sent to Meta.
## 5.714.4 — September 25, 2026 at 2:43 AM UTC
* `request_proposals` now reports age targeting that was already resolved from your brief text alongside its existing `demographicOverlay` capability line, so a product with brief-driven age no longer reads as if age targeting failed. `demographicOverlay`/`ageTargeting` are unchanged and still describe only the separate AdCP 3.2 structured-overlay path, which remains unavailable while the platform negotiates AdCP 3.1.
## 5.714.3 — September 25, 2026 at 2:20 AM UTC
* `create_media_buy` no longer permanently blocks retries after a seller rejects a request that never reached the ad server (for example, an unsupported bidding combination). A corrected resubmission on the same media buy now gets a fresh attempt instead of an idempotency conflict. Rejections where the seller may have partially processed the request still block retries, so no duplicate ad-server write can be created.
## 5.714.1 — September 25, 2026 at 1:31 AM UTC
* Storefront task webhook callbacks now recover after transient delivery failures without reusing a delivery identity when the event timestamp changes.
## 5.714.0 — September 25, 2026 at 1:02 AM UTC
* Inventory source pages now lead with the saved connection, authentication state, and next action. Technical test evidence stays under Technical details, and connecting a Partner referral keeps the selected Source open in the same browser tab.
* Inventory source validation and sandbox guidance now explains the account, authorization, and provider test setup that still apply. Selecting or omitting a Source no longer implies that a blocked connection becomes runnable or that every provider account is no-spend.
* Valid API and MCP requests queued behind another authorization lookup now receive a fresh bounded lookup window, reducing temporary authentication failures during request bursts. Authentication rules and permissions are unchanged, and no action is required.
* Source Diagnostics now opens faster for storefronts with multiple inventory sources. If one source is unavailable, the page still shows evidence from the others. No action is needed.
## 5.713.0 — September 25, 2026 at 12:19 AM UTC
* Accept completed synchronous adapter delivery payloads that omit the transport-managed task status.
* Every published agreement page now lists its earlier versions at the bottom, each linking to its permanent page, so you can open the exact text of any version you or a counterparty accepted. Opening an older version shows only the versions published before it. The list comes from the agreement registry, so a newly published version appears automatically. For agents and integrations, `GET /agreements/{document}/versions` returns the same list, newest first.
## 5.712.0 — September 24, 2026 at 11:44 PM UTC
* On the Advertisers and Campaigns Pages, a Dimensions filter in the filter row narrows the list to the labels you pick, such as Market › US or Launch › Q4, and each card shows its current labels as read-only chips. To change one campaign's labels, open the campaign and use Labels in its detail view (shown when your account can manage dimensions); to label advertisers, create a dimension, or label many campaigns or advertisers at once, ask in chat, then reopen the Page to see the change. Nothing changes for reporting, targeting, or seller Pages.
* Sellers now see a campaign's goals when the buyer requests proposals. The brief each seller receives carries the campaign's ranked optimisation goals with their targets (for example "clicks, target cost 3.00 or less per click, counted by the seller's delivery metrics"), so a seller can price against the goal instead of guessing it from prose. Event goals disclose the event type and never the buyer's event source id or tracker configuration, and the campaign budget stays withheld.
* When you read a media buy with creatives, each creative now includes its saved click-through URL. Nothing to change on your side.
## 5.711.2 — September 24, 2026 at 11:25 PM UTC
* Media Partners now keeps selected and default-on partners in Active while advertiser setup is provisioning, deferred, failed, or waiting for action. Available is reserved for partners you can add or have excluded. You don’t need to take any action.
## 5.711.1 — September 24, 2026 at 10:55 PM UTC
* Fixed an edge case in seller event-source provisioning: renaming an event source (or changing its event types) after it was already provisioned onto a seller account now correctly re-registers it with the seller instead of potentially reusing a stale response.
## 5.711.0 — September 24, 2026 at 10:40 PM UTC
* Meta campaign connections can now finish multi-page campaign syncs even when Meta rewrites its pagination URL. Nothing changes on your side.
## 5.710.1 — September 24, 2026 at 10:23 PM UTC
* Apostra connection authorization links now remain on the Apostra domain. Existing Interchange links are unchanged, and no reconnection is required.
## 5.709.3 — September 24, 2026 at 9:33 PM UTC
* Buyer agents now receive safe diagnostic details when a connected provider campaign sync fails, so they can identify the next corrective action instead of stopping at a generic error.
## 5.709.2 — September 24, 2026 at 8:56 PM UTC
* Apostra signup, confirmation emails, and FAQs now say that we will get in touch to enable access, without describing Apostra as a private alpha or asking people to join a waitlist.
## 5.709.1 — September 24, 2026 at 8:36 PM UTC
* Publish the buy-side Apostra commercial offer in the Organization IU Rate Card guide. Connecting accounts, creating campaigns, and placing buys are free with 0% of media. Optional Intelligence is in beta; the existing Enhanced Reporting catalog default is 4 IUs per connected account per billing period and current plan templates waive it, while other buy-side Intelligence plan pricing is coming soon. Enterprise services are scoped and priced through a separate agreement. Ordinary product support through Murph remains included.
* The internal model defines what must be true before a buyer intelligence activity can be priced, keeps bulk IU economics separate from feature access, and treats each Enterprise engagement as a scoped combination of managed service, integrations, cooperative agent development, dedicated support, governance, and intelligence.
* The pricing source also separates partner-funded gateway economics from the buyer offer, records the current one-year Global Market Maker negotiating anchor, and explains how seller-response density can grow IU revenue without introducing a percentage-of-media fee.
* Enabled Creative Engines and organisation advertiser grants are less likely to be interrupted by a slow feature-flag response. Nothing to change on your side.
## 5.709.0 — September 24, 2026 at 8:01 PM UTC
* `get_delivery` now supports additive `live_campaign_delivery` reads for one exact campaign from its connected provider. Pass `campaignId` and a date range. The existing stored `campaign_delivery` rows, totals, filters, dimensions, and pagination contract remains unchanged.
* The Apostra developer package uses this live read for its read-only Meta campaign review. When Apostra or Meta authorization is missing, the agent gives the person a secure browser handoff instead of asking for credentials.
* Meta campaign pagination now strips provider-echoed authorization parameters from trusted continuation URLs and follows them with the current connection credential. This restores campaign synchronization without forwarding a token from response data.
* When a provider referral has no suitable Agent Source yet, you can create one from that provider's published connection options without leaving the referral. Enter a full HTTPS endpoint only when the connection options allow a publisher-owned endpoint. Your endpoint and credential reference stay with your Source; they do not change another publisher's connection or the Agent's shared identity. Provider acceptance, validation, activation, and certification remain separate steps.
## 5.708.2 — September 24, 2026 at 7:41 PM UTC
* Valid API and MCP calls using the same API key now handle request bursts with fewer temporary authentication failures. Authentication rules and permissions are unchanged, and there is nothing to change on your side.
## 5.708.1 — September 24, 2026 at 7:10 PM UTC
* When you test a brief from a specific inventory source, the preview now reports that source's catalogue error instead of saying the storefront has no inventory. Healthy sources with no matching products still return an empty preview, and buyer discovery is unchanged. Nothing to change on your side.
## 5.708.0 — September 24, 2026 at 6:33 PM UTC
* Restore Campaign image thumbnails, make rehosted Media Partner logos visible across API instances without blocking page loads, and use the current product brand in portable widget hosts. Keep account-switch recovery on the selected account when provider refresh fails.
* Embedded sales agent 1.45.3: verify FreeWheel currency after booking.
## 5.707.1 — September 24, 2026 at 5:39 PM UTC
* Account-scoped media buys now keep the buyer's authentication when syncing creatives during creation and updates, so the sync can reach the seller.
## 5.707.0 — September 24, 2026 at 5:27 PM UTC
* Event sources now register automatically on every seller account that supports it, not just the ones that happened to find them on their own, like Meta sometimes did. When a campaign referencing your event source buys on a seller account such as Snap or Meta, Interchange registers the source there, and every later buy sends the seller's own id instead of yours, so that seller can attribute conversions back to your campaign. Nothing to change on your side — this starts on your next buy against that seller, and it can add up to a few seconds of latency to that buy while it registers (capped at five seconds); it never blocks the buy outright. If the seller can't confirm an id in time — a failed registration or a timeout — that buy proceeds without that one optimization goal rather than sending an id the seller was never given, and it's retried automatically on your next buy. A seller that doesn't support this at all is rechecked at most every 30 days instead of on every buy. Check the `sellerProvisioning` array on `GET /advertisers/:advertiserId/event-sources` for the status per seller: `provisioned`, `needs_install` (some sellers require a one-time manual step in their own dashboard before they issue an id), `unsupported` (nothing to configure — that seller's native tracking still applies; rechecked every 30 days), or `failed` (fix the named error and it retries on your next buy).
## 5.706.0 — September 24, 2026 at 4:58 PM UTC
* Campaigns now gives every buyer the enriched list with advertiser-wide active campaign count, daily budget, and today's spend.
* Update media buy — typed Storefront targeting preflight. A `targeting_overlay` your seller doesn't support (for example a `geo_postal_areas` country outside the seller's declared coverage) now fails early with a clear `Sales agent does not support the requested targeting: …` error, before the seller-side dispatch, instead of failing later inside the adapter with an adapter-shaped message. No change to any accepted overlay.
## 5.705.0 — September 24, 2026 at 4:34 PM UTC
* Keep buyer and seller left-rail destinations as stable navigation after a chat starts.
## 5.701.0 — September 24, 2026 at 2:10 PM UTC
* You can now commit product-local creative formats in modular avails feeds without a matching publisher catalog entry. Omit `publisher_domain` for a product-local format and provide a stable `format_option_id` when possible; existing requests without one receive a deterministic id. Publisher-scoped formats still require an exact catalog match.
* If you choose Developer when signing up with a password, email verification now returns you to the signup you started. No action is needed.
* Sellers using third-party catalog sources will keep supported zero-reserve CPM auction products in catalog validation. No action is needed; invalid price data remains excluded.
## 5.699.0 — September 24, 2026 at 12:28 PM UTC
* Account Admins can now open **Attest current endpoint** from an eligible Agent's Overview when an observed production revision still needs endpoint confirmation. Certification setup errors now distinguish a missing revision endpoint, a missing protected fixture, an unavailable fixture, and an unsafe fixture source. Attesting an endpoint does not register a fixture, start certification, or authorize spend.
* Managed ad-server-backed storefronts now keep creative format selections aligned with their media-buy packages, so valid buys reach the connected ad server instead of being cancelled before submission.
## 5.698.0 — September 24, 2026 at 12:05 PM UTC
* Source Health now distinguishes an Apostra initial-response timeout from an error returned by your third-party sales agent. It assigns the timeout to Apostra, keeps later completed responses healthy, and still assigns genuine Source errors to your operator.
## 5.697.1 — September 24, 2026 at 11:04 AM UTC
* Creative Library searches now return exact creative results reliably when render links are available.
* Campaigns and Creative Library pages opened in a new chat now stay above the chat input and starter prompts. You do not need to change anything.
* Demand Inbox now shows saved and uploaded RFPs when you open it from the seller navigation or an existing bookmark. Nothing to change on your side.
## 5.697.0 — September 24, 2026 at 10:31 AM UTC
* Fixed a bug where launching a Meta "Sales — Maximize number of conversions" (or Traffic) media buy could fail with a `SERVICE_UNAVAILABLE` error and roll back the campaign, even though the ad account was fully eligible. Meta returns an additional geo-targeting detail on these campaign types that our targeting verification didn't yet recognize; it's now accepted, and the campaign launches normally.
* The advertiser roster no longer overlaps the chat input or starter prompts when the list fills the available screen height.
## 5.696.1 — September 24, 2026 at 9:56 AM UTC
* Buyer agents can now express country, ISO 3166-2 region, metro, and postal targeting on a V3 media-buy overlay, including exclusions. Country and region names resolve to canonical codes before dispatch. Campaign geographic intent is used as the default for proposal discovery and media buys unless a buy supplies its own overlay.
## 5.695.0 — September 24, 2026 at 7:47 AM UTC
* Product Marketing links now open the Library, where sellers can continue to manage material and review selling points without moving or re-uploading data.
## 5.694.1 — September 24, 2026 at 7:12 AM UTC
* Sellers with a configured custom modular source can now ask Murph to open a read-only preview for that source, then plan and run a confirmed no-spend test campaign using its products. The test leaves source, storefront, and Listing activation unchanged. Nothing to change on your side.
## 5.692.1 — September 24, 2026 at 5:08 AM UTC
* Re-running product discovery on a campaign now always uses the campaign's current brief, even when the request reuses an existing discovery ID. Previously, editing a campaign's brief (for example to add geographic targeting after a launch error) had no effect on a subsequent discovery run against that same discovery ID — sellers kept receiving the original, pre-edit brief.
* Reading a V3 campaign with no creatives attached now says plainly that creatives are optional before launch and only needed once delivery starts, instead of showing an unexplained "0 creative(s)" count. Buyer agents were misreading that bare count as a launch blocker and telling buyers to attach a creative before a campaign could go live, even though nothing on the platform required one.
## 5.692.0 — September 24, 2026 at 3:39 AM UTC
* Use dimensions to label and filter Creatives and reusable Creative Library assets. Existing Library tags are available through the built-in `tags` dimension, and the Library can filter by labels without exposing another advertiser's data.
## 5.691.0 — September 24, 2026 at 2:55 AM UTC
* Organization administrators can now review and change access for an approved self-serve advertiser relationship from the Advertisers page. They can choose Viewer (`read`), Editor (`read` and `write`), or Administrator (`admin`, `read`, and `write`) after comparing currently reviewed, currently effective, and proposed access. These changes do not grant billing, payment, or Campaign execution authority.
## 5.690.0 — September 24, 2026 at 2:25 AM UTC
* Buyer advertisers can now sync first-party CRM audiences through the V3 MCP surface using `save_audience`. This enables audience-based social targeting (Meta, TikTok, and similar channels) that requires a first-party seed list from the advertiser's CRM. Without this surface, the targeting layer had no way to receive that seed list through the agent.
* Each call accepts one or more audience items. For each audience, the agent can add members by raw email (normalized and SHA-256 hashed at the API boundary before forwarding), pre-hashed email, or pre-hashed phone; remove members by external ID; or delete the audience entirely. Deletion and removal are permanent for that sync; re-adding members requires a subsequent sync call.
* After submission, use `get(kind: audience, advertiserId: )` to check match status and member counts as processing completes.
* This capability ships ungated (`gate: none`): it operates on the caller's own advertiser data, is governed by existing GDPR/consent fields on each audience item, and does not alter any shared platform state. No additional rollout cohort or kill switch is needed beyond the existing buyer account access control.
* **Target persona:** This tool is built for the S2S/MCP-integrated buyer — the scrappy-builder or power-buyer persona who programmatically manages CRM-audience-based social campaigns (Meta, TikTok, and similar channels) via an automated pipeline. Enterprise and agency buyers who prefer a business-effect framing: this capability enables CRM-based social targeting without a separate data-onboarding service. Sellers are not implicated: `save_audience` operates on the buyer's own advertiser data, has no effect on storefronts, ranking, or seller presentation, and is invisible to any seller account.
* **Monetization:** This capability is part of the standard buyer MCP surface with no separate billing line.
* **Acceptance criteria:** (1) A buyer calling `save_audience` with a valid audience item receives a stable `operationId`; (2) A subsequent `get(kind: audience, advertiserId: )` returns the bounded `total` and audience rows with their processing status and member counts within the V2 sync SLA. These criteria are covered by the `v3-audience-surface` acceptance test suite.
* **Adoption measurement:** `save_audience` call volume and `syncAudiences` dispatch count are captured in the existing audience-sync V2 metrics pipeline — no new instrumentation is required. Review cadence: weekly for the first 30 days post-launch to confirm adoption and surface any match-rate anomalies.
## 5.689.0 — September 24, 2026 at 1:23 AM UTC
* Buyer readiness now counts only sellers with active sales agents, aligning the ready count with the active-agent eligibility used for proposal requests.
* Remove the Pages & slides tab. Pages and slides now open inside each document, and sellers can filter the Documents list to show material that has them.
## 5.688.0 — September 24, 2026 at 12:48 AM UTC
* The Apostra Platform Terms of Service v4.1 are now the current terms. If you're an organization admin on v3.0, you'll see a prompt to review and accept v4.1 when you next open Apostra. You can choose **Review later** until October 23, 2026; after that, account activity pauses until an admin accepts. Until you accept, your current contract and rates stay in place. Once you accept, your standard contract takes no percentage of media. Negotiated contracts are never prompted. The documents v4.1 incorporates are published with it: Facilitated Agreement Terms 1.2, which govern your existing Relationship Agreements once you accept, Marketplace Policies 1.1, and Payment Operations Schedule 1.1. See what's different from v3.0: [https://api.apostra.com/agreements/changes/terms-of-service/v3-0\_v4-1](https://api.apostra.com/agreements/changes/terms-of-service/v3-0_v4-1)
## 5.686.2 — September 23, 2026 at 11:37 PM UTC
* Interactive pages now keep their height in sync when content loads after the host connection, and long advertiser directories use the available page space. Campaigns, notifications, and other widgets remain fully visible with nothing to change on your side.
## 5.686.0 — September 23, 2026 at 11:07 PM UTC
* You can now create, update, archive, and restore event sources — the pixels and server feeds your campaigns measure conversions through — with the new `save_event_source` tool, and read them with `search`/`get` alongside your other campaign objects. Existing sources and sync calls are unaffected: nothing to change on your side.
* `event_source` is distinct from `measurement_source` (your MMM feeds, brand-lift studies, and other outcome-record sources): use `event_source` to register how conversions reach us, `measurement_source` for how you measure their effect afterward.
* Each event source can now also report the ISO 4217 currencies it accepts for ROAS reporting. It can report where its events originate too — a flat category like website, app, or in-store, or, when that's too coarse, a structured surface such as a specific owned channel or feed. And it carries a new AdCP health grade, from `insufficient` to `excellent`, alongside the ingestion status you already had.
* Syncing event sources with `event_sources` omitted is now a discovery-only call: it returns every registered source unmodified, so you can check what's configured before you write.
* Distribution pricing is now available on request. Existing sellers can accept a Distribution upgrade with no monthly fee for its first 60 days, and the accepted offer controls the IU rate. Listing waives brief responses, Enhanced Reporting, and qualifying media buys. Storefront agent plans waive Enhanced Reporting and charge 1 IU for each completed brief response and qualifying media buy.
## 5.684.0 — September 23, 2026 at 10:22 PM UTC
* Review extracted selling points and inventory disagreements from each document in the Library.
## 5.683.2 — September 23, 2026 at 9:55 PM UTC
* Sales-agent validation can now confirm its sandbox advertiser activation when the storefront is not yet eligible for live transactions. Normal advertiser activation and storefront eligibility are unchanged, with nothing to change on your side.
## 5.683.1 — September 23, 2026 at 9:16 PM UTC
* The campaigns list in chat no longer gets cut off at the bottom.
* It was reporting its height to the chat host only in a timing window that rarely happened, so the host often left the panel at its startup height and clipped everything below the filters — including the campaigns themselves and the panel's own rounded bottom edge. The list now reports its height reliably, and a long list scrolls inside the panel instead of running past its edge.
* Archived campaigns are no longer mixed into the list by default. Pick "Archived" in the status filter to see them.
* For buyers on the V3 campaigns experience, the list also now uses the same visual system as the rest of the product — the same cards, status labels, filters, and empty states.
* The Creatives page now opens for seller-operated advertisers in the Advertisers workspace. Nothing to change on your side.
## 5.683.0 — September 23, 2026 at 8:17 PM UTC
* To replace a saved Interchange connector name in Claude, ask an organization owner to remove it and re-add the same `api.apostra.com` MCP endpoint with the name Apostra. New connections through `api.apostra.com` use Apostra's purple apostrophe icon and publish Apostra as the server identity automatically.
## 5.682.5 — September 23, 2026 at 8:00 PM UTC
* Buyer account switches now keep the account menu covered until the selected account is ready. Nothing needs to change on your side.
## 5.682.4 — September 23, 2026 at 7:36 PM UTC
* This changeset records the launch guidance and exchange-ledger change as evidence, not a customer capability.
## 5.682.3 — September 23, 2026 at 7:16 PM UTC
* The Demand inbox now keeps every saved brief visible when one row's details cannot load. The affected row explains the gap and can be retried while the other rows remain current. Nothing to change on your side.
## 5.682.2 — September 23, 2026 at 6:33 PM UTC
* Fixed a bug where a buyer's creative-approval webhook could get permanently stuck after a single delivery failure and never retry successfully. A background query that only needed to read the request's retry count was also touching its `updated_at` timestamp, which the delivery system relies on to recognize a retry as the same notification; that side effect made every subsequent retry look like a conflicting delivery, so the buyer's push notification for that creative's approval or rejection was never delivered.
## 5.682.1 — September 23, 2026 at 5:52 PM UTC
* Connections pages now show provider cards without waiting for brand-registry lookups.
## 5.682.0 — September 23, 2026 at 5:27 PM UTC
* Cancelling or deleting a media buy whose seller acceptance is still pending now succeeds and releases its budget; the seller-side cancel completes once the seller resolves the buy.
* Seller Rate Card plans now carry their own activity prices. Listing plans waive all IU-rated seller activity, while Merchandising plans charge only for work completed by the Storefront agent.
* Listing-only storefronts no longer show Demand or specialist merchandising navigation, while storefronts with merchandising selected keep those destinations during setup.
## 5.680.0 — September 23, 2026 at 4:32 PM UTC
* Sign in with Google or Microsoft now opens the provider's account picker when you have not typed an email first, so you can choose which account to sign in with. Previously the provider signed you straight in with whatever account your browser already had a session for, which made the provider buttons look like they only worked after typing your email. Typing your email before clicking a provider still takes you straight to that account.
* Signing in now completes when your browser opens the identity provider in a new window instead of the current tab. Arc does this, and it left sign-in dead on "Authentication failed — Sign-in verification has expired. Please start again." even on a first attempt. The state a sign-in carries from start to finish — the verification secret, the email you typed, and the MCP client session — is now readable by the window that receives the provider's callback, so you end up signed in, on the page you were heading to, with the wrong-account check still applied. Sign-ins started in separate windows stay independent of each other, and unfinished sign-in state is discarded after thirty minutes.
## 5.679.0 — September 23, 2026 at 4:09 PM UTC
* Demand now stays in the storefront navigation, while the chat add menu focuses on files and other content you can add.
## 5.678.1 — September 23, 2026 at 2:39 PM UTC
* The advertiser picker in Murph's left rail reads better on a long roster. Its menu no longer runs the height of the window: the list scrolls inside a fixed height, with the search field pinned above it and "+ Add advertiser" pinned below, and a row part-way through a scroll is no longer sliced flush against either of them. The menu is a little wider than the button that opens it, a sandbox advertiser's label now sits beside its ID rather than on its name, and with no advertiser chosen the button reads "All advertisers" instead of the account name.
* Private plans now keep the renewal terms shown in the plan and accepted offer instead of losing automatic renewal because they are not publicly listed.
* Staging a v3 media buy package for Meta, Snap, or TikTok with no geographic targeting now returns a non-blocking warning immediately, instead of only failing at launch after budget, creative, and flight work is already done.
## 5.678.0 — September 23, 2026 at 2:12 PM UTC
* V3 campaign launch failures now show the real classified reason (and, where the platform's message is safe to display, the specific text) instead of a bare "execution failed" — a debug-mode error wrapper was silently dropping that information before it reached the buyer.
* Connections and other media-partner pages no longer wait for third-party logo rehosting before showing content.
* Sellers can now re-admit a rejected buyer account intake. A new `POST /api/v2/storefront/account-mappings/reviews/:intakeId/readmit` endpoint transitions a rejected intake back to `pending_review` so the seller can review it again - useful when a rejection was made by mistake.
* V3 tool responses now surface more detail by default. `save_signal` success text includes the signal ID and source ID so follow-up calls can reference the exact record without a separate search. `save_media_buy` create responses include the seller, flight dates, and budget; update responses include the resulting flight; and update errors include the attempted flight dates. Error messages from `save_advertiser`, `save_connection`, and `save_wholesale_product` now include the relevant entity ID and operation context, making it easier to diagnose what was attempted when a write fails. Signal discovery now runs with debug mode enabled so diagnostic detail is available when discovery fails.
## 5.677.1 — September 23, 2026 at 1:12 PM UTC
* Fixed the API access page showing a generic "internal error" when a parent account admin was not enrolled in a sub-account's WorkOS organization. The error is now correctly classified as an access issue (HTTP 403 with a clear message) rather than an opaque internal server error.
* Cancelling or deleting a media buy that is still waiting for seller approval now withdraws the seller's approval request, so the seller can no longer approve it and it can never go live afterwards. Previously the cancel failed and left the request approvable, so a buyer who cancelled a buy could still see it booked with the seller's ad server once the seller approved it. The buy reads canceled only once the request is withdrawn; if the seller approved it moments before your cancel arrived, the cancel is refused and you can retry it once the buy reaches the seller's ad server. Nothing to change on your side. Sellers: a buy the buyer cancelled drops out of your approvals queue and shows as revoked.
* Cleaned up the wording buyers' agents see when Meta or TikTok has no real price guidance to offer. It now reads "no price guidance available for this cpm product" instead of the awkward "cpm AUD (no price guidance available)".
## 5.676.2 — September 23, 2026 at 12:13 PM UTC
* Updated the documentation site's content to the Apostra brand: company/product references, API and asset URLs (`interchange.io` → `apostra.com`), and the "Use Apostra"/"Build with Apostra" guide filenames now match the rest of the docs.
## 5.676.1 — September 23, 2026 at 11:45 AM UTC
* Launching a v3 campaign with a Meta media buy that has no geographic targeting now shows the specific reason ("Meta requires explicit geographic targeting...") instead of a generic "The ad platform rejected the request as invalid." message.
## 5.676.0 — September 23, 2026 at 11:08 AM UTC
* Buyers on MANAGED storefronts can now declare `operator` or `advertiser` billing on a `sync_accounts` entry, not only `agent`. `POST /api/v2/buyer/storefront-accounts/sync` and the `sync_buyer_storefront_accounts` tool accept a `billing_entity` payload on any entry — legal name, tax/VAT/registration ids, postal address, billing contacts, and (optionally) bank details — and `operator`/`advertiser` entries now require one.
* A storefront only accepts the billing modes it has declared support for; requesting one it doesn't returns HTTP `400` `BILLING_NOT_SUPPORTED` naming the supported modes. `billing_entity.bank` is write-only: you can send it to provide payment coordinates, but it is never echoed back in a `sync_accounts` or `list_accounts` response. `billing: 'agent'` behavior, and entries that omit `billing_entity` for agent billing, are unchanged.
* **Sellers see no new protocol or UI surface.** A buyer's `billing_entity` flows through the same account-grant record sellers already read for `agent` billing.
* **Monetization:** no billable surface, no packaging change.
* **Rollout - gate: none, atomic launch.** The new billing modes and `billing_entity` field are additive; a buyer that never sends them sees no behavior change. Release check: after deploy, an `operator`/`advertiser` entry with a complete `billing_entity` against a storefront that supports it returns `action: created`/`updated`, one against a storefront that doesn't returns `BILLING_NOT_SUPPORTED`, and no response body carries `billing_entity.bank`.
* Meta's `get_media_buy_delivery` adapter method now accepts host-attested `protocolOptions.servedAdcpVersion` and forwards it into the underlying task, matching the signature of Meta's other four AdCP methods. No customer-visible behavior changes today — the delivery-read task has no 3.2 branches to gate yet — but this closes the last Meta signature-parity gap so a subsequent per-behavior slice can add 3.2 read-path branches without touching the adapter boundary.
* Meta and TikTok product discovery and proposal responses no longer show a \$0 or invented CPM floor price. Neither platform gives us a real price signal at discovery time, so a floor we authored ourselves was never more than a guess - and for any non-USD account it was already showing as zero. We now leave floor price and price guidance out of the response entirely when we don't have a real number to back it, rather than showing a number that could be mistaken for platform guidance.
* Snap wholesale `get_products` now stamps per-product `ext.scope3_execution_readiness` (ready\_for\_execution / setup\_required / input\_required / selection\_required / inventory\_unavailable) when the buyer's Snap ad account is connected, matching Meta's and TikTok's existing wholesale behaviour. Buyers on Snap wholesale can now see at discovery time whether a product will succeed at `create_media_buy` or requires setup.
## 5.675.1 — September 23, 2026 at 10:33 AM UTC
* Library cards and rows now show file-type thumbnails and preview state, while buyer Creative images use their approved source image when available.
## 5.675.0 — September 23, 2026 at 10:21 AM UTC
* Open Buyer Account Mapping directly from the Buyers workspace. Murph now uses the public guide when explaining where buyer mappings are managed.
* Fixed a bug where budget and other field updates sent to AdsWizz/Mediamark (and any ESA seller that responds synchronously) were confirmed by the seller but not applied locally - leaving the media buy showing its old values indefinitely. Updates now correctly apply after seller confirmation.
* The TypeScript starter can generate types from the live tool schemas. Run `npm run types:generate` to write one input type per tool into `src/generated/tools.ts`, so incompatible type or enum changes and newly required fields can become compile errors instead of rejected calls.
## 5.674.0 — September 23, 2026 at 9:27 AM UTC
* The Media Partners page now groups partners into Active, Needs connection, and Available, and opens with Ready for buying selected. Choose All to include partners that are not yet open for buying; partners that need a platform connection remain visible.
## 5.673.0 — September 23, 2026 at 8:43 AM UTC
* First-party emails now use the Apostra name, legal identity, and visual design from the scheduled launch time. Buyer invitations have shorter setup copy.
* Buyer waitlist submissions now receive a confirmation email after the form is accepted. The message confirms that the request is under review without implying that an account or access already exists.
## 5.671.0 — September 23, 2026 at 7:17 AM UTC
* Show sellers recorded reasons for an RFP response, including a safe link to the relevant source diagnostics when pricing configuration is incomplete.
## 5.670.0 — September 23, 2026 at 5:53 AM UTC
* Seller Library documents and Creative Library items now open in a previewer while the browse list stays in place.
## 5.667.0 — September 23, 2026 at 4:04 AM UTC
* Restrict Apostra's developer skill to canonical first-party contract sources, scan its exact source tree for trust-boundary drift before publication, and bind the pre-publication policy result to the exact source digest, including extensionless text and executable assets, without treating an unbound public provider result as release qualification.
* Give an Account Admin an owner task for attesting the endpoint of an exact current observed Sales revision. Fixture setup now reports a safe reason when it has no eligible option. Attestation records endpoint provenance only: it does not register a fixture, certify an Agent, start a test, or enable spend.
## 5.665.0 — September 23, 2026 at 3:08 AM UTC
* Uploading to the Library after an earlier session no longer fails with "Could not start the private upload".
* Responses now show only sent or imported proposals. Uploaded Library files stay in Documents until you save one as a response from its detail view by linking it to a brief.
* In V3 `get_delivery`, group campaign delivery by a Buyer Account label with `dimensions: ["labels."]`. Reports use current campaign and advertiser labels, so relabeling reorganizes historical delivery; no setup change is required.
## 5.664.0 — September 23, 2026 at 2:10 AM UTC
* Buy mobile web display, CTV, and other coherent inventory combinations in one campaign by defining channel groups with `save_campaign`. Assign every selected product or proposal to a `channelGroupId`; Interchange creates separate media buys for each group, seller, and settlement currency, and reports delivery by the same group. Presets cover display, online video (OLV), mobile web display, mobile web OLV, and CTV, or you can define a custom group with AdCP channel, property type, device type, and format values.
* Channel groups preserve the buyer's declared inventory intent, but Interchange does not yet filter discovery, validate selected products against a group, or translate the group into package targeting. Clients must verify seller product evidence before assigning a group. See [Channel groups](/v2/buyer/campaigns/channel-groups) for examples and reporting queries.
* Library now identifies a private upload that did not finish as incomplete and lets you retry the original file, instead of leaving the document at Inspection pending.
* Launching a v3 campaign with a Meta media buy staged without geographic targeting now returns a clear reason ("Meta requires explicit geographic targeting...") instead of an opaque, unclassified failure with no diagnostic detail.
* When transaction validation finds an active inventory source that is not linked to a registered Agent, Interchange now opens Source diagnostics for that exact source. You can create or attach the Agent there, then retry validation.
## 5.663.0 — September 23, 2026 at 1:46 AM UTC
* Exact document reads return fenced code blocks and inline code again. The search tokenizer strips them so ranking works on prose; reads shared that projection, so request examples were removed from the surface agents use to learn call shapes.
* When you open the Advertisers page, advertiser cards now load campaign counts without downloading every campaign or waiting for every brand logo. A logo that is still loading uses the existing monogram fallback. There is nothing to change on your side.
* Switching accounts now stays inside the application instead of loading the whole page again. The account menu and page content wait for the new account together, which removes the brief flash of the previous account. Wide Advertisers tables also fit the available page width.
* The Sellers page now starts loading seller data immediately even when its optional configuration service is slow. The Connections page also loads its initial data with fewer requests.
* When OAuth authorisation for a sales-agent source fails, the credentials page now shows the specific safe error instead of a generic failure. Interchange also waits until it has observed the Agent before offering implementation registration. Nothing changes on the agent server.
* The Library now opens as a list grouped by category, and the grid view stays one click away.
* build-with-apostra 1.0.3 corrects the instructions so agents take request and response shapes from the tool schemas before writing calls or parsers. It also separates authentication and account readiness from operation authority. This prevents local fixtures from confirming an assumed shape that the server can reject.
* Use the full public Git URL when adding the Apostra developer marketplace in Codex, Claude Code, or another compatible coding agent. Existing installations do not need to change.
## 5.662.0 — September 22, 2026 at 10:54 PM UTC
* The advertiser picker in Murph's left rail now edits any advertiser directly from its row: hover a row and a pencil opens that advertiser's settings without changing which advertiser you are working in. From the keyboard, the up and down arrows choose an advertiser and the right arrow edits it. The roster no longer repeats itself as a second list of nav rows under the picker, each row labels its identifier as `ID: …` rather than showing a bare string, and the picker's search field drops its magnifying-glass icon. On a storefront, the setup checklist count in the rail header reads `4 of 14` with a chevron instead of underlined text, so it looks like the link into the checklist that it already was.
* Murph now keeps chat available while you use Sellers and Creative Engines, and Creative Engines loads and scrolls correctly. Nothing to change on your side.
## 5.661.1 — September 22, 2026 at 10:31 PM UTC
* The Library now explains what each tab is for and what to do next, and its rail item no longer shows the materials-teaching count.
## 5.660.1 — September 22, 2026 at 8:32 PM UTC
* Advertisers with a confirmed no-country-qualifier brand scope can now manage sellers after selecting at least one channel. Legacy advertisers can classify an unknown country scope once, and the form now explains exactly which setup is required for seller availability.
## 5.660.0 — September 22, 2026 at 8:24 PM UTC
* The released Apostra Platform Terms of Service v4.1 are available at their permanent versioned URL. Existing product terms and acceptance requirements are unchanged.
## 5.659.0 — September 22, 2026 at 8:12 PM UTC
* When you attach a canonical Creative in the Creative Library, Interchange now checks its declared media details against the campaign's product options. It shows whether the Creative fits, needs named adaptations, or lacks the facts needed to confirm a fit; if no option fits, attachment stops and identifies the closest option and the mismatch. Attachments with insufficient facts proceed unconfirmed. Existing attachments and legacy seller-specific formats do not change. An attachment records the required adaptations but does not create them. The Creative is forwarded as-is for seller review.
* The Add button in the Library is no longer covered by the widget's collapse control. Header actions in Listing, Murph escalations, Release notes, buyer Catalog list and detail, Run-rate, and Marketing email operations also remain clear of it.
## 5.657.0 — September 22, 2026 at 7:23 PM UTC
* Event (measurement) sources now report `managedBy` (`buyer` or `seller`) and `sellerId` on every read — via `GET /api/v2/buyer/advertisers/:advertiserId/event-sources`, `sync_event_sources`, and the `measurement_source` noun (`search`, `get`, and `save_measurement_source` in the v3 agent surface). Buyer-created sources report `managedBy: "buyer"` with `sellerId: null`. These fields are read-only for now; a later release will populate `sellerId` once a source is provisioned on a seller account.
## 5.656.1 — September 22, 2026 at 6:11 PM UTC
* Media buy reads now show newly attached and removed creatives immediately instead of returning a stale creative list for up to five minutes.
## 5.656.0 — September 22, 2026 at 5:53 PM UTC
* The agent that sells for your storefront is now called the **storefront agent** in the docs glossary and concept guide. It was previously documented as the Merchandising Agent; the glossary keeps that name as a pointer so older links and screenshots still make sense. Nothing changes in how the agent works or in your account.
## 5.655.2 — September 22, 2026 at 5:08 PM UTC
* Sales-adapter dispatch now forwards the host-attested AdCP 3.2 negotiation to the underlying adapter tool call. Previously, `adapterCallOptions()` in the storefront SDK platform adapter propagated `candidatePropertyCoverageVersion` but silently dropped `servedAdcpVersion`, so Meta's 3.2 code branches (and any other adapter's 3.2 wiring) never executed for storefronts that route through a sales adapter. Direct-dispatch storefronts were already correct; this change closes the sales-adapter parity gap.
* Tidied two pages that sit on their own outside the app shell. The page you land on from a "connect this adapter" link now keeps one frame the whole way through — asking for a key, waiting for the provider, and explaining a dead end all look like the same page rather than three different ones — and its buttons take the standard size. The page for accepting an invitation to an organization uses the standard loading state on its Accept button.
* TikTok wholesale `get_products` now stamps per-product `ext.scope3_execution_readiness` (ready\_for\_execution / setup\_required / input\_required / selection\_required / inventory\_unavailable) when the buyer's TikTok ad account is connected, matching Meta's existing wholesale behaviour. Buyers on TikTok wholesale can now see at discovery time whether a product will succeed at `create_media_buy` or requires setup.
## 5.655.1 — September 22, 2026 at 4:42 PM UTC
* The Apostra company page now uses the brand’s uppercase display treatment for its “Better advertising” headline and gives the supporting headings the same visual weight as the copy beneath them.
## 5.655.0 — September 22, 2026 at 4:28 PM UTC
* Tidied the standalone pages that sit outside the app shell. The hosted "add a payment method" page and the media-company confirmation form now use the shared panel, so their headings, spacing, and buttons match every other page. The referral page is now two tabs — make a referral, then see what became of the ones you have made — instead of one long column, and an empty list says so properly rather than trailing a grey sentence. The referral form's fields now show what to type in them, the company-confirmation form no longer shows an empty country box, and the note about what an AMC account includes now sits with the option it describes.
## 5.654.0 — September 22, 2026 at 4:06 PM UTC
* Account administrators can inspect commercial identity, correct operator domains, look up public branding and follow Seller domain reviews from a text-only v3 MCP client. Identity reads expose saved-brand provenance and verification status in text; identity writes return audit receipts.
* Use `identityContract: "confirmed-v1"` on operator identity saves and existing advertiser brand corrections. Before replacing an operator identity or advertiser brand, request `preview: true`. Review the change with the person requesting it, then repeat without `preview` and with its confirmation token. An unconfirmed replacement in this contract returns an error and saves nothing. Earlier save payloads retain their existing behaviour. Advertiser creation can require public branding with `publicBrand: true` and a stable idempotency key. See [Identity and brands](https://docs.interchange.io/v2/setup/v3/identity-setup) for the calls and verification steps.
* Domain text and public branding do not grant access or prove ownership. Editing or publishing `brand.json` and completing Buyer AAO OAuth proof are outside this text-only flow; the guide explains the supported human follow-up.
## 5.653.1 — September 22, 2026 at 3:32 PM UTC
* Embedded sales agent 1.45.2: bound update media buy idempotency replay.
## 5.653.0 — September 22, 2026 at 3:23 PM UTC
* Allow a directly authenticated organization account admin to attest a current observed Sales revision's existing source endpoint without changing the revision or its deployment history.
## 5.652.3 — September 22, 2026 at 3:02 PM UTC
* Codex and Claude Code installation now uses the published Apostra package. The developer guide verifies account access with a read-only call and keeps interactive sign-in separate from a deployed application's runtime identity.
* Budget changes made while a media buy is waiting for seller approval now update the pending request. If the request cannot be updated, the buyer's original budget is kept.
## 5.652.2 — September 22, 2026 at 2:35 PM UTC
* Media-buy updates now use a new idempotency key for each submitted change, so a previous failed response cannot be replayed over a later retry with the same budget.
## 5.652.1 — September 22, 2026 at 2:04 PM UTC
* Seller analytics compares buyers side by side again, and a screen reader can reach every part of it
* Buyer signals and recent runs are tables once more — one row per buyer or run, every comparable figure in its own column — so two buyers can be read against each other without scanning down two cards. When the table is wider than the widget, its scroll area is now reachable from the keyboard and announced by name, so the off-screen columns are not stranded.
* Each widget title is a heading, so heading navigation lands on it, and a policy's own headings sit underneath in order. Metric names read as written names rather than machine keys: the latency tile says "Average processing latency" instead of "Average Processing Latency Ms".
## 5.652.0 — September 22, 2026 at 1:24 PM UTC
* Murph's left navigation is rebuilt on the Apostra design system. Sections that showed a collapse chevron now actually collapse — previously several of them drew the chevron but ignored the click. The setup checklist drops its per-section progress fractions, keeping the single "N of M steps" summary at the top, and its rows now read at the same size as the rest of the rail.
## 5.651.3 — September 22, 2026 at 1:01 PM UTC
* Creative Library now explains how to save a draft with a canonical format or a seller product format, and shows when Creative Engines is unavailable.
* `save_campaign` responses now include flight dates, budget, phase, and seller IDs in the text content so the full campaign state is visible without a separate read. When a campaign launch fails, each per-buy error now includes the exact ESA request payload that was sent, making it easier to confirm what targeting, pricing, and format options were passed. `save_media_buy` staging calls now also pass debug mode through so diagnostic detail is available when media buy staging fails.
* The standalone pages behind the account menu and the organization settings — your account, account settings, notification preferences, connected apps, user API keys, member management, advertisers, activity, reporting, connections, billing sections and partner fleet — are rebuilt on the Apostra design system. Same pages, same actions, same data; what changes is the typography, spacing and controls, which now match the rest of the product instead of carrying the older look.
* A filter that sits above a list no longer repeats its own name as a label — the chosen value already says what it filters.
* More of the product now uses the new design system: the API keys page, the account page, member management, advertisers, connected apps, notification preferences, organization settings, buyer agents, campaign creative assets, and the "page not found" / "access denied" / error screens. Nothing about how any of them work has changed — the same information, the same actions, the same permissions. A newly created API key is still shown once and never again.
## 5.651.2 — September 22, 2026 at 12:22 PM UTC
* Media buys sent to legacy-format sellers no longer fail when a creative is pinned to a specific format option. After the storefront translates the package's format options into the seller's legacy formats, the pinned creative had nothing to match against and the request was rejected with "cannot select a valid creative wire shape ... did not provide one unambiguous legacy format reference". The pin is now matched exactly against the legacy format it was created from, and a creative that narrows to a single legacy format is trafficked under it. A pin that names a format the product does not offer still fails, with a message that says so.
## 5.650.2 — September 22, 2026 at 9:57 AM UTC
* Credential alerts now name the connected platform, explain what has stopped, and link directly to Settings > Connections to reconnect it.
## 5.650.1 — September 22, 2026 at 9:35 AM UTC
* Fresh Demo Storefront chats reach the Teach entry without waiting for unrelated setup navigation.
## 5.650.0 — September 22, 2026 at 9:18 AM UTC
* Buyers can now open the campaign Creative Library composer and assemble a creative from their uploaded assets even when Creative Engines is not enabled. Pick assets, fill format slots, and save the assembly draft without connecting or calling an engine. Generation remains available only to Buyer Accounts with Creative Engines enabled; when it is unavailable, the composer labels those controls and links to setup guidance.
## 5.649.0 — September 22, 2026 at 8:36 AM UTC
* Buyers can now use Nielsen DMA codes or familiar DMA names when saving campaign and media-buy targeting. If a name could mean more than one DMA, the response lists the matching codes to choose from.
## 5.648.0 — September 22, 2026 at 7:09 AM UTC
* Organize advertisers and campaigns with buyer-owned dimensions and labels. Create governed or open dimensions, use the built-in `tags` dimension, and find labeled or unlabeled records through the V3 MCP tools. Buyer Accounts can have up to 20 dimensions, each dimension up to 200 values, and each advertiser or campaign up to 50 labels. Existing advertisers and campaigns start unlabeled. Nothing to change on your side.
## 5.647.0 — September 22, 2026 at 6:28 AM UTC
* An advertiser can now hold more than one catalog, and your agent can read and write each one directly on the V3 agent surface. A catalog is the AdCP Catalog object: a product feed or other typed list your advertiser sells from. Find them with `search(kind: "catalog")`, read one with `get(kind: "catalog", id, advertiserId: "12345")`, and add `include: ["items"]` to page through its items up to 50 at a time, filtered by id, GTIN, tag, category, keyword contains query, or `status: "approved"`. Create, replace, or archive a catalog with one `save_catalog` call; it requires an idempotency key, so a retried request returns the original result or recorded failure for 24 hours instead of applying twice. Each catalog reports a health of `healthy`, `syncing`, or `stale` with a plain-language reason. Nothing to change on your side: an advertiser's existing default catalog keeps working as before.
## 5.646.0 — September 22, 2026 at 5:40 AM UTC
* When you assemble a creative in the Library, products now update automatically while sellers respond and are easier to search and compare. Product rows show the declared format requirements and explain when an asset does not fit, so you can upload another file or review all available products before you save.
## 5.645.0 — September 22, 2026 at 5:15 AM UTC
* Buyers can now turn an uploaded file into a creative by naming its format before choosing a product.
## 5.644.3 — September 22, 2026 at 3:36 AM UTC
* `get { kind: "advertiser" }` now returns `preferredTimezone` alongside `reportingTimezoneLocked`, matching `save_advertiser` and `search`. Previously, a buyer reading an advertiser via `get` could see that its reporting timezone was locked but not what it was locked to.
## 5.644.2 — September 22, 2026 at 2:09 AM UTC
* Fixes AI-9720: buyers can recover from cancelled or stale media-buy updates without a stranded pending change blocking their next update.
## 5.644.1 — September 22, 2026 at 12:15 AM UTC
* `save_campaign`'s launch step now shows the actual reason a media buy failed to launch when that reason is safe, locally-generated text from a first-party ad-platform adapter (e.g. Meta requiring a Pixel/Dataset for a Sales campaign) - previously the chat response collapsed every failure to a bare `execution failed (CODE, recovery)` with no further detail, even when the underlying cause was already known and actionable. A genuine remote sales agent's own error text is unaffected and continues to stay out of the rendered response, unchanged from today's behavior.
* Let a verified domain user claim an archived Seller account, become an admin, and restore its existing Storefront. This works for every archived Storefront, including accounts that already have an administrator, while keeping ambiguous domains and public email providers out of the recovery path.
## 5.644.0 — September 21, 2026 at 11:42 PM UTC
* Upload copy now says file, not creative, and clarifies Library file types, audio availability, campaign counts, and Catalogs so buyers can find and use their creative files.
* Pending invitations in the account menu now open the organization you were invited to, even when you are working in a different organization. Sign-up links for invitations that are no longer available now explain the problem before account creation instead of asking you to join an unnamed organization.
## 5.643.0 — September 21, 2026 at 10:37 PM UTC
* Seller signup now describes storefront setup more clearly and labels the setup action directly. No action is needed for existing accounts.
## 5.642.3 — September 21, 2026 at 10:22 PM UTC
* Account choices use the available row width during sign-up when fewer than three options are available.
* Adapter errors on `create_media_buy`, `update_media_buy`, `sync_creatives`, and delivery reads (`get_media_buy_delivery`, `get_media_buys`) now return a classified error message and code instead of the ad platform's raw response text. This closes an inconsistent gap where Google, LinkedIn, Reddit, Criteo, CitrusAd, Flashtalking, OpenAI Ads, UniversalAds, and Pinterest passed their provider's unfiltered error text straight through on every operation, and where `sync_creatives` could leak raw upstream diagnostics in a creative's `errors[]` entry regardless of platform. `code`/`reason` remain the same stable, machine-actionable classification every integration already keys on; the platform's own explanation (when it designed that text for external display, as Meta does) is now surfaced consistently instead of only in a couple of hardcoded scenarios.
## 5.642.1 — September 21, 2026 at 8:43 PM UTC
* Campaigns Pages pass plain advertiser names to their widgets, and browser links open the advertiser's workspace.
## 5.642.0 — September 21, 2026 at 8:16 PM UTC
* On the buyer agents list, the link to the documentation moves into the page header beside Add agent, so it stays reachable when the list is empty or when you are signed in through a session that cannot manage agents — the states where it was previously hidden. Each agent row is now a card, and its status carries a colour that matches what the state means for that agent's work.
## 5.640.3 — September 21, 2026 at 7:04 PM UTC
* The buyer agent list and agent detail pages are rebuilt on the Apostra design system. Same routes, same actions, same data — what changes is the presentation and some of the wording.
* Each tab's sections no longer draw a panel inside the page's own panel, so a section reads as a band of the page rather than a nested card. A credential is one row that names itself and carries its own Rotate and Revoke actions. On Activity, "Platform state changes" and "Administration activity" say in plain language what separates them: what the platform recorded on its own, versus what someone on your team did and who they were. A limitation under "Current limitations" now reads as a warning when there is something to act on, and as an empty state when the connection simply has not reported anything yet.
## 5.640.0 — September 21, 2026 at 5:47 PM UTC
* Branding & distribution now opens Listing even when the browser had cached an older setup route. Publisher domains entered in the Property Roster are saved directly; invalid entries stay unsaved, and a failed save keeps your draft so you can try again.
## 5.638.0 — September 21, 2026 at 4:40 PM UTC
* Selecting the required-step count in the setup rail now opens the full Seller Setup checklist. The rail labels the final validation separately, so its result no longer looks like the full checklist.
## 5.637.0 — September 21, 2026 at 3:51 PM UTC
* The Creative Library now leads with **Add creative**. Pick an image or video file; when the upload finishes, the Library opens the creative composer (campaign Library) or the inline creator (advertiser Library) with that file in the first slot, and you complete the creative with a name, click-through URL, and format. Campaigns that declare Audio :30 also accept audio. **Upload** (add a file only) and **New creative** (start from a file already in the Library) are under **More**. Nothing to change on your side.
* Media buys sent to sellers that still speak legacy creative formats no longer fail when the seller lists more than one legacy format for the same display size, for example `display_html` with 300x250 dimensions next to `display_300x250_html`. The creative is now matched on its own declared dimensions and, when two legacy formats impose the same constraints, trafficked under the seller's fixed-size format. Previously the request was rejected with "cannot select a valid creative wire shape ... the seller advertised 2 legacy refs".
* Text across the product is easier to read. Interface copy, labels, and table data now set in Public Sans, a typeface drawn for screen text at small sizes, while headings keep the GT Planar brand face. Small text reads clearer without anything moving or changing size, and labels separate more cleanly from the content around them.
* In-chat widgets also load less: each one carries about 29 KB less font data than before.
## 5.636.0 — September 21, 2026 at 3:05 PM UTC
* `save_media_buy` on the v3 buyer surface now accepts `products[].pixelId`, so Meta Sales campaigns can supply the required Pixel/Dataset ID for conversion tracking. Previously the field had no way to reach v3, so launching a Meta Sales (or a Traffic campaign optimizing toward a pixel-based conversion goal) failed with a bare `INVALID_REQUEST` naming no cause. This brings the field over from the existing v2 `create_media_buys` surface unchanged - same name, same "no auto-select" behavior.
* Storefront transaction readiness now stays current while product catalogues are syncing.
* Find the right advertiser faster by searching the workspace selector by name or advertiser ID. Advertiser IDs now appear alongside names, making similar or duplicate names easy to distinguish.
## 5.635.0 — September 21, 2026 at 2:47 PM UTC
* After uploading a creative asset, see its preview, name, media type, size, and saved Library tags, then open that exact advertiser-scoped Library asset.
## 5.632.0 — September 21, 2026 at 12:54 PM UTC
* Widget navigation now keeps text readable while buttons change state. Nothing changes on your side.
* Interactive buyer Pages now accept Claude's external-host authorization when Claude forwards the same signed Page capability in both request metadata and the reserved app argument. Conflicting capabilities and first-party proxy requests remain rejected.
* Canonical image requests now reach the configured creative generator with their validated dimensions. Existing valid legacy format selectors continue to work; malformed or ambiguous selectors are rejected before generation.
## 5.630.0 — September 21, 2026 at 11:32 AM UTC
* Select or drop up to 25 creative files into the upload Task and follow them in one progress list. The Task uploads three files at a time, and a failed file retries on its own without touching the ones that finished. Enter Library tags once for the whole batch, or change a queued file's tags before its upload starts. Per-file size limits are unchanged, and a batch adds files to the Library; it does not create Creatives. Nothing to change on your side.
## 5.628.1 — September 21, 2026 at 10:45 AM UTC
* Murph can now follow an exact Skill ID without searching for it first.
* Library and Product Marketing uploads now accept a valid short-lived private upload reservation when the seller's browser clock is slightly behind the server. Server reservation failures remain distinct from an unsafe upload capability, and overlong capabilities still fail closed.
## 5.628.0 — September 21, 2026 at 10:14 AM UTC
* Interactive buyer Pages (Creative Library, Advertisers, Campaigns) now load in Claude and other MCP-app hosts that run the widget on a separate MCP session from the chat. Opening a Page returned "authorization could not be verified" on its first read in those hosts because the Page's signed capability was bound to the session that launched it; it is now verified against its own signed session when it arrives through the external-host transport, with every other check unchanged.
* Embedded sales agent 1.45.1: create bookable FreeWheel placements.
## 5.625.0 — September 21, 2026 at 8:41 AM UTC
* Creating a Meta media buy with postal/zip-code targeting for most countries (United States, United Kingdom, Canada, Germany, Switzerland, Austria, France, Australia, Brazil, India) now succeeds instead of failing after the ad set was already created on Meta.
* Buyer agents can open the Creative composer from the Creative Library's new campaign-scoped composer lens, including a browser hand-off for hosts without an embedded widget surface.
## 5.623.1 — September 21, 2026 at 6:57 AM UTC
* Fixed `request_proposals` returning a bare, unexplained `SELLER_ERROR` for a built-in storefront adapter (e.g. Meta) rejecting a request as invalid — for example, a brief that names an unsupported placement exclusion like "Threads" or "Audience Network". The adapter's specific, actionable error code and message now reach the response instead of being discarded.
## 5.623.0 — September 21, 2026 at 6:03 AM UTC
* Third-party sales-agent observations now remain current when two refreshes reach the same connection at once. This keeps implementation changes and freshness records available for certification. Nothing needs to change on your side.
* Murph can now read an exact versioned Skill before continuing a workflow.
## 5.621.0 — September 21, 2026 at 3:08 AM UTC
* Buyers can now open a slot-based Creative composer from a campaign's Creative Library. Save drafts to an existing Creative Session, preview slot changes as you work, and use host chat to request copy or other changes.
## 5.620.0 — September 21, 2026 at 2:43 AM UTC
* Meta `discover_products` and `create_media_buy` now honor a buyer's request to target Facebook/Instagram Stories and/or Reels alongside Feed. Previously, any brief or `targeting_overlay.placement_selection` naming Stories or Reels silently collapsed to Feed only. A mixed request — Feed together with Stories and/or Reels — is now honored in full. `targeting_overlay.placement_selection` is also now read and honored at `create_media_buy` time (previously rejected outright as an unsupported targeting dimension) and can override the discovery-time snapshot, including an explicit `mode: "default"` to revert to Advantage+ Placements.
* Requesting only Stories and/or Reels with no Feed in the mix remains unavailable on every account today: discovery now returns no products for that specific request rather than one it can't actually deliver.
* Brief-text placement parsing also now fails closed on an explicit exclusion (e.g. "excluding Feeds, only Stories and Reels") instead of matching the excluded surface's keyword and selecting it anyway.
## 5.618.0 — September 21, 2026 at 12:16 AM UTC
* Organization administrators can approve, suspend, reactivate, or revoke one advertiser's self-serve admission from the Advertisers page. These actions do not change capabilities, invitations, billing or payment authority, organization membership, or campaign execution.
## 5.617.0 — September 20, 2026 at 11:07 PM UTC
* The Library now identifies Responses as durable saved materials and shows each saved version’s date and current brief. Opening Responses does not regenerate an answer from the documents visible in your Library. Nothing to change on your side.
## 5.616.1 — September 20, 2026 at 10:29 PM UTC
* You can now see and manage a brief’s linked response from either the Demand Inbox or the Library. Choose an eligible saved response or import a historical brief without losing the current pairing record. Add files in the Library before linking them from the Demand Inbox. Nothing to change on your side.
## 5.615.0 — September 20, 2026 at 9:12 PM UTC
* MP4 upload preparation now finishes within the upload Task's request window. Storage or signing timeouts and confirmed database aborts return a retryable error without signed upload details; an unconfirmed database outcome says not to retry. JPEG, PNG, WAV, and MP3 uploads are unchanged.
* Opening the Campaigns Page from a host that cannot show interactive pages, such as Claude Code or a plain MCP client, now returns an "Open in the browser" link that opens the same Campaigns Page in the Interchange chat for that advertiser, focused on the launched campaign when there was one. The link is also available as `openInBrowserUrl` in the structured result. Following a Campaigns Page link in the browser now opens the Page once, with the list filters the link carries, instead of minting a second launch that was discarded on arrival.
## 5.613.0 — September 20, 2026 at 7:39 PM UTC
* In the Library Page, sellers can assign a document category and filter documents by category alongside file type, search, and grouping. Existing documents remain Uncategorized until a seller chooses a category. Rate-card and policy documents can remain source Materials, but their canonical facts remain governed by Rate Cards and AI Business Rules. Categorising a document does not make its units reusable.
## 5.612.2 — September 20, 2026 at 7:04 PM UTC
* Existing storefronts with audited pre-split sales-agent sources can continue transacting without repeating onboarding approval.
## 5.612.0 — September 20, 2026 at 6:24 PM UTC
* Library now explains reuse per page, slide, or sheet, shows why each unavailable control is disabled, and enables eligible units as soon as inspection finishes. Keep buyer-specific content off and current prices in Rate Card.
## 5.611.0 — September 20, 2026 at 6:07 PM UTC
* Buyers can now see connected catalog feeds in the advertiser Creative Library, including item counts, sync health, and links to manage a feed.
* An empty Material Library links to a fictional publisher onboarding pack with a short, goal-based starter checklist. The guide distinguishes included examples from missing proof material and keeps pricing and policy in their owning flows. Opening or downloading the pack never adds account data, and completing the checklist proves neither seller readiness nor production completeness.
## 5.608.0 — September 20, 2026 at 4:09 PM UTC
* You can open the Create Agent task from the Agents page. Your existing administrator permissions still apply.
* Keep navigation to another account workspace from returning you to a Murph conversation that is no longer available.
## 5.604.0 — September 20, 2026 at 2:11 PM UTC
* Set which campaigns a Library creative runs on from either side of the relationship: `save_creative` now accepts `campaignIds`, the full set of campaigns for an advertiser creative, and `save_campaign` accepts `creativeIds`, the full set of creatives on a campaign. An explicit list replaces membership, so a missing id detaches; omitting the field leaves membership unchanged. Attaching checks the campaign's sellers and fails, naming them, when none accepts the creative's format. Both fields set the same membership the Creative Library picker sets, and a member creative is delivered on every media buy of the campaign whose products accept its format.
## 5.601.2 — September 20, 2026 at 11:38 AM UTC
* Show both Enterprise seller choices during signup while keeping them outside the selectable public Rate Card plans.
## 5.601.0 — September 20, 2026 at 10:28 AM UTC
* Murph now states that you are interacting with an AI agent on your first interaction. Once delivered, the notice applies across supported channels. This does not change what Murph can access or do.
* Every Apostra sign-up path now stays in the Apostra interface, including organization invitations, buyer qualification, partner registration, and Storefront plan setup. Nothing changes for existing accounts.
* Murph in Slack now clears its working status after posting a completed response. Nothing to change on your side.
## 5.600.0 — September 20, 2026 at 9:36 AM UTC
* Correct the historical identity used to restore readiness for one existing third-party sales source. New sources still need the normal validation.
* Your enrolled advertiser viewers can now list Campaign summaries for the advertiser assigned to them. Their access is read-only and does not extend to another advertiser.
## 5.599.2 — September 20, 2026 at 9:24 AM UTC
* Live seller workspaces now open analytics even when setup status is temporarily unavailable. Nothing to change on your side.
## 5.598.1 — September 20, 2026 at 8:29 AM UTC
* Apostra-branded sign-in pages now show the full Apostra wordmark with dark lettering.
## 5.597.0 — September 20, 2026 at 2:38 AM UTC
* Opening a creative in the Creative Library now offers "Attach to a campaign". The picker lists the advertiser's campaigns with their status, flight and budget, and says for each one how many of its sellers accept this creative's format and which sellers do not and why. Campaigns whose flight has ended, and campaigns where no seller accepts the format, are shown with the reason instead of an Attach button. Attaching adds the creative to the campaign so its sellers review it; campaigns it already belongs to are marked Attached.
* An existing third-party sales source that was serving before readiness enforcement no longer remains blocked because its historical transaction evidence was missed. New sources still need the normal validation.
## 5.596.0 — September 20, 2026 at 1:00 AM UTC
* Saving coaching in a practice proposal now reruns the same buyer brief and opens the revised response beside the original.
* When a seller-managed relationship needs missing advertiser information, Sellers now pauses the advertiser view with one setup action instead of repeating it on every seller card. Direct ad platforms do not trigger this blocker. Nothing changes for API integrations.
## 5.593.1 — September 19, 2026 at 9:18 PM UTC
* Campaign Creative links now open the advertiser Creative Library with the campaign already selected, while keeping Creative requirements, upload, create, attach, and preview actions together.
* Seller workspaces now open directly into setup or analytics without briefly showing the Merchandising Agent introduction. Nothing to change on your side.
## 5.593.0 — September 19, 2026 at 8:30 PM UTC
* Buyer access requests now use Apostra's responsive sign-up experience when the new product brand is enabled. The journey keeps its existing qualification and routing behaviour while making conditional questions, validation, progress and outcomes easier to follow on desktop and mobile.
* Buyer access requests now distinguish direct Apostra use, a buyer's own AI agent, and a preferred AI chat. Buyers can select Claude, ChatGPT, Copilot, or name another chat, or say that they are still undecided. Pilot countries and channels appear in the intended priority order, Social requests can proceed regardless of country, and New Zealand is no longer treated as a pilot market. Buyers who select Social or a pilot country receive the same fit confirmation at any timing. The nurture message now explains that supply is still being onboarded in the selected markets.
* Managed MP4 upload is now generally available and no longer depends on a separate account rollout switch. It remains subject to authenticated advertiser access and managed-video service availability. Managed audio remains an alpha behind its existing account rollout gate and independently dark production infrastructure.
## 5.592.0 — September 19, 2026 at 7:53 PM UTC
* Buyers can associate a completed advertiser Creative with an authorised campaign through `save_creative` before seller package assignment or provider delivery exists. The saved Creative read now reports durable campaign membership separately from delivery state.
## 5.589.0 — September 19, 2026 at 3:36 PM UTC
* Agent Page can now forward a protected technical certification evaluation request to Murph. A queued request is not a completed evaluation, certification, or client authorization.
## 5.588.0 — September 19, 2026 at 3:24 PM UTC
* During an active Demo Storefront evaluation, Product Marketing now names the selected file and distinguishes upload, processing, ready-to-review and failure states. Each state gives the seller one clear next action, while ordinary Product Marketing and the existing private upload flow remain unchanged.
* Opening the Creative Library from a host that cannot show interactive pages, such as Claude Code or a plain MCP client, now returns an "Open in the browser" link that opens the same Library in the Interchange chat for that advertiser. The link is also in the structured result as `openInBrowserUrl`.
## 5.587.0 — September 19, 2026 at 2:58 PM UTC
* Stop sending `sellerPractice` and `sellerPracticeAction` in Murph chat requests. Seller Practice is no longer available, and requests containing either field now return a validation error instead of opening an ordinary chat. Other seller chat requests work as before.
## 5.586.2 — September 19, 2026 at 2:32 PM UTC
* During an active Demo Storefront evaluation, Product Marketing now starts with a clearly labelled file upload and explains the path from adding seller material to reviewing offers and testing a buyer brief. Webpage import and synthetic practice remain available as secondary choices. Ordinary seller Product Marketing and upload handling do not change.
## 5.585.1 — September 19, 2026 at 1:33 PM UTC
* Practice proposal reruns now apply the latest saved coaching note while keeping the buyer brief, products, prices, sources, and claims unchanged.
## 5.584.1 — September 19, 2026 at 12:49 PM UTC
* The fixed synthetic practice proposal now uses only the seeded Demo catalogue and its matching synthetic teaching material. Ordinary media-kit uploads and Library examples stay out.
## 5.584.0 — September 19, 2026 at 12:12 PM UTC
* Opening a creative in the Creative Library now shows where it stands with each seller. A Seller approval table lists every seller the creative was sent to with Approved, Reviewing, Changes requested, Rejected or Sync failed, the date, and the seller's note when it rejected the creative or asked for changes, and the header sums it up as "Approved n of m" plus how many need attention. A collapsed Destination row shows the click URL, the URL as delivered, and which UTM additions came from the advertiser, campaign or creative. A creative not yet sent to any seller says so.
## 5.583.2 — September 19, 2026 at 12:07 PM UTC
* Slack actions that need your approval now say “Approval needed” while they wait, instead of reporting that the call failed. The action, approval decision, and final result do not change.
## 5.583.1 — September 19, 2026 at 11:35 AM UTC
* Library previews now distinguish pending, failed, unsupported, unavailable, and ready files, and older PDF or PowerPoint uploads can be retried from the open document without reloading the Library.
## 5.582.1 — September 19, 2026 at 10:00 AM UTC
* Inventory-source diagnostics now retain recorded health timestamps so an older error is not presented as a new provider check or current transaction outcome.
* Time-bounded Seller beta profiles now include the complete feature package they name. Enrolled sellers no longer need a second set of matching entitlement grants, and the profile's expiry still returns the account to its safe fallback.
## 5.582.0 — September 19, 2026 at 9:40 AM UTC
* The Creative Library page is easier to scan. Creatives and Assets are tabs with counts, one row holds search and the Shelf, Format, Source and Role filters (Media for assets), and each card shows its state alongside role, campaign and reuse badges. Opening a creative or asset shows a detail with About, Used in, Where it lives and a collapsed technical record, and "New creative" opens the composer directly. The same layout fits the narrower width chat hosts give a widget.
## 5.581.3 — September 19, 2026 at 9:26 AM UTC
* Buyer briefs now open as compact cards with the complete plan, targeting, measurement, governance, provenance, and exact request available in one disclosure. Existing brief records remain readable, and sellers do not need to change how they work.
## 5.581.2 — September 19, 2026 at 9:08 AM UTC
* V3 archive is now visibility-only: campaigns and media buys keep their lifecycle phase, and restoring a canceled campaign keeps it canceled. Campaign archive refuses while executable media buys remain, and legacy archived media buys are readable.
* Buyer account reads now keep a linked seller account visible when its connection is disabled or archived, so it can be reconnected. Partner-scoped reads now include direct seller-account links.
## 5.581.1 — September 19, 2026 at 8:37 AM UTC
* In Advertisers, clients admitted for self-service now show their advertiser access state instead of a missing sponsorship warning. These rows remain read-only; access readiness does not grant Campaign or payment authority. Nothing to change on your side.
## 5.580.2 — September 19, 2026 at 6:29 AM UTC
* Sellers in the Product Marketing beta can complete a teaching rehearsal from their training material to a held-out buyer brief, a coached follow-up proposal, and its comparison.
## 5.580.1 — September 19, 2026 at 5:54 AM UTC
* Record a completed, cleaned own-supply transaction as the matching technical-certification assertion only after the platform verifies its immutable fixture and implementation binding.
## 5.580.0 — September 19, 2026 at 1:37 AM UTC
* Show the confirmed sample booking, synthetic source, and delivered impressions in Demo Storefront Media buys.
## 5.579.3 — September 19, 2026 at 12:08 AM UTC
* Product searches that exceed the quick wait now show the products already returned and can continue the same search without starting a replacement. Nothing needs to change on your side.
* Active Demo Storefronts now show a synthetic, non-commercial Plan & billing state without changing ordinary billing setup.
## 5.579.2 — September 18, 2026 at 11:18 PM UTC
* Synthetic inventory sources in an active Demo Storefront now show all nine required setup steps as ready. Source Diagnostics identifies them as local synthetic recipes and no longer asks you to run vendor connection checks that do not apply. Ordinary inventory sources keep their existing live-source checks.
* Fixed an issue where buyer-agent principal routes were inaccessible for organizations whose WorkOS binding exists but whose internal organization record has not been populated. The registry key for buyer-agent principals is now derived from the customer ID directly, which is always present, rather than from the optional organization FK.
## 5.579.1 — September 18, 2026 at 10:06 PM UTC
* When you open Listing in an active Demo Storefront, its readiness now agrees with Seller Setup: Identity and What you do are the two required details. Marketplace publication and Public distribution remain available as optional choices. Nothing changes for ordinary Storefronts, and there is nothing to change on your side.
* Pages on app.apostra.com now show the Apostra name and favicon in browser tabs. Apostra's staging app and creative-preview domains use the orange Apostra symbol to stay visually distinct. Other domains are unchanged, and no action is required.
## 5.579.0 — September 18, 2026 at 9:17 PM UTC
* Media Companies can now create a sandbox advertiser from Advertisers or Campaigns and switch into it immediately. Accounts enrolled for live campaign management can explicitly choose Live; other accounts see Sandbox only.
## 5.578.2 — September 18, 2026 at 8:58 PM UTC
* Selecting a Demo Storefront inventory source now replaces the previous source Page and opens the source named in the setup checklist.
* Demo Storefront now shows its versioned sample buyer brief and booked outcome in Demand while the demo lease is active.
## 5.577.1 — September 18, 2026 at 7:56 PM UTC
* Readable scanned faxes now reach your storefront agent when lower scan quality makes text recognition less certain. Nothing to change on your side.
## 5.577.0 — September 18, 2026 at 7:36 PM UTC
* Demo Storefronts now create and restore a synthetic inventory source during explicit creation and reset, so Inventory and Signals can be explored without connecting a live ad server.
## 5.576.0 — September 18, 2026 at 7:22 PM UTC
* Click-URL parameters now have one home. The `utmConfig` you set on an advertiser or campaign and the `tracking.macroAdditions` you set through V3 are the same parameters, resolved advertiser → campaign → creative with the nearest value winning, and applied once when a creative is synced to a seller. A key already in your click-through URL is never overwritten. New advertisers start with no parameters; advertisers created earlier keep the six `utm_*` entries Scope3 used to add for them, and you can edit or clear those like any other entry. `{PACKAGE_ID}` is accepted alongside the canonical AdCP macros. Parameter keys start with a letter or digit and use URL query-key characters (max 100), the same rule for `utmConfig` and `macroAdditions`.
## 5.575.0 — September 18, 2026 at 6:55 PM UTC
* Embedded sales agent 1.45.0: add synthetic mock inventory selectors, add synthetic mock signal catalog, negotiate FreeWheel booking actions as XML, preserve published format option ids, update AnyIO past known vulnerabilities.
## 5.574.1 — September 18, 2026 at 6:33 PM UTC
* Keyboard focus on a clickable row in an in-chat widget now draws its focus outline inside the row, so the outline is no longer sheared off at the left and right edges when the list scrolls. Advertiser rows on the All advertisers widget also sit flush against each other instead of being separated by a gap.
## 5.574.0 — September 18, 2026 at 6:05 PM UTC
* Buyers on MANAGED storefronts can now request **production** account linkages, not only sandbox ones. `POST /api/v2/buyer/storefront-accounts/sync` and the `sync_buyer_storefront_accounts` tool accept entries without `sandbox: true`, and the request lands in the seller's approval queue as a pending account. Once the seller accepts it and maps the account to a source, creative sync on that storefront stops failing with `MANAGED_ACCOUNT_LINKAGE_REQUIRED`.
* A production entry must use your confirmed buyer operator domain as `operator`. Any other domain returns HTTP `403` `ACCESS_DENIED` (`details.reason: "operator_identity_mismatch"`); an account without a confirmed identity gets the existing `BUYER_SETUP_REQUIRED` response. Sandbox entries are unchanged and still accept any operator. `billing` remains `agent`.
* **Sellers see no protocol or UI change.** Production requests arrive through the same pending-review queue and decision endpoints as any inbound `sync_accounts`.
* **Monetization:** no billable surface, no packaging change.
* **Rollout - gate: none, atomic launch.** The operator check is the only new refusal and it fails closed. Kill switch is `apps/api/src/routes/v2/buyer-accounts.routes.ts` (unmount) or re-adding the `sandbox` literal in `apps/api/src/schemas/v2/buyer-accounts.schema.ts`. Release check: after deploy, a production `dry_run: true` request from a buyer with a confirmed identity returns `action: created`, and one naming another operator returns 403; adoption watch is the count of non-sandbox `storefront-accounts/sync` requests per customer via the standard v2 REST metric labels.
## 5.573.0 — September 18, 2026 at 5:36 PM UTC
* Demo Storefront approval queues now start with one synthetic media-buy request and one synthetic creative review. Reset restores both pending items, while expiration or deletion removes their synthetic approval data.
## 5.571.0 — September 18, 2026 at 4:15 PM UTC
* In `save_campaign`, set `brief` or `flight` to `null` to clear the saved value. Omit either field to leave it unchanged. Campaign pauses now use the same audited path as the API.
## 5.570.0 — September 18, 2026 at 3:46 PM UTC
* Claimed agent owners can request a protected certification evaluation for the current implementation when an approved fixture and immutable evaluation template already exist. The request does not run a provider test or create certification evidence.
* Show a claimed agent owner when protected certification preparation cannot continue, rather than scheduling the same permanent failure again.
## 5.568.1 — September 18, 2026 at 2:46 PM UTC
* Platform administrators can now choose Listing, Listing + Distribution, Merchandising, or Merchandising + Distribution capabilities when creating a Demo Storefront. New and existing demos default to the full Merchandising + Distribution experience without changing the organization's paid plan. Setup progress also counts only required work, while recommended setup remains visible.
## 5.568.0 — September 18, 2026 at 2:07 PM UTC
* Buyer agents can now create, inspect, update, and archive advertiser event sources through V3. Each save returns installation guidance and keeps registration separate from proof that conversion events are flowing.
* Buyer agents can also define generic click-URL macro additions on advertisers, campaigns, and creatives. Campaign values override advertiser defaults, creative values override both, and `null` suppresses an inherited parameter without changing the authored click URL.
* Murph’s guided Storefront rehearsal now explains the purpose, current step, next action, and expected result across Teach, Confirm, Test, Inspect, and Coach. It uses the same seller workflow and actions as self-guided practice. Media-kit upload is discoverable, teaching still requires explicit candidate confirmation, and coaching still records immediately on the immutable turn.
## 5.567.0 — September 18, 2026 at 12:26 PM UTC
* Advertiser guidance now explains how measurement evidence stays in advertiser scope across sellers and distinguishes Apostra's standard 0% media-fee model from percentage-based terms in existing agreements.
* Reject a media buy when its composed product points to a creative-format option that the source product has since removed, renamed, or changed. The storefront now asks the buyer to refresh the product before approval or forwarding instead of sending a request the source will reject.
## 5.565.0 — September 18, 2026 at 11:06 AM UTC
* For organizations enrolled in IU plan selection, seller choices now appear under Listing and Merchandising. Choose Get in touch for help with a plan or a private offer, even when public plans are available. A billing administrator can send the request and, when tracking is available, follow it in Your requests. Stalled help requests now recover instead of leaving plan actions disabled. During signup, finish creating your account, then send the request from Plan & Billing. Your plan, prices and access stay the same until you separately accept an offer.
## 5.563.0 — September 18, 2026 at 9:23 AM UTC
* Embedded sales agent 1.44.0: add FreeWheel inventory-package targeting and ad-product defaults, support FreeWheel direct ad-unit selection, harden FreeWheel booking state reconciliation, keep AdsWizz package delivery stable across renames, persist and book FreeWheel insertion orders, respect FreeWheel insertion-order booking state, traffic AdsWizz audio impression trackers, validate FreeWheel VAST renditions after create.
## 5.562.0 — September 18, 2026 at 8:47 AM UTC
* The Merchandising Agent interview now stays open when you enter the seller workspace, so Seller Analytics no longer replaces the buyer-practice entry.
## 5.561.0 — September 18, 2026 at 8:04 AM UTC
* Installable messaging apps are now available as an early-access customer feature: your team can talk to Murph from Slack, Microsoft Teams, Discord, or WhatsApp. On a seller's Listing page, Distribution lists each as a destination with two ways in; seller administrators manage the same apps from Settings → Messaging apps. **Use the Interchange app** is the shared, one-click option with nothing to configure. Slack installs from the tab once Interchange's Slack app is publicly available; shared Microsoft Teams and Discord apps are not available yet. **Use your own app** connects a Microsoft Teams, Discord, or WhatsApp app you created with the provider. The tab says what each provider needs, lists the apps already connected with their install locations and linked people, and opens the secure setup pane (Settings → Messaging apps) where you manage credentials, approved install locations, and the links between provider identities and Interchange members. WhatsApp replies only when a linked member messages the registered business number inside Meta's user-initiated service window; it never starts a conversation or sends template notifications.
* Branded apps are early access for enrolled accounts. Enrollment controls who can set up and administer them; it does not stop an app that is already active. To stop new conversations, suspend a location or retire the branded app. Nothing changes for storefront ranking, readiness, or buying workflows, and there is no new plan, entitlement, or usage charge. Conversations from Microsoft Teams, Discord, and WhatsApp never count toward usage or billing; they are recorded for telemetry only.
* Team members who already use Murph need no setup of their own. Once an administrator has connected an app and a member's Teams, Discord, or WhatsApp identity is linked to their active Interchange membership, that member can ask Murph the same questions from an approved Teams or Discord chat, or by replying on WhatsApp. Members never handle provider credentials or installations.
## 5.560.5 — September 18, 2026 at 6:57 AM UTC
* Creative reads now report a successful sync only when every current seller link succeeded. Failed links include the seller's rejection reason when available, so buyers can see that a creative was not delivered and why. Nothing to change on your side.
## 5.560.3 — September 18, 2026 at 5:59 AM UTC
* Enterprise offers now reject unsupported campaign management, custom inventory onboarding and co-marketing commitments before they become commercial terms. No action is required for offers that do not include those commitments.
## 5.559.0 — September 18, 2026 at 4:29 AM UTC
* Files from your existing creatives can appear in the Creative Library Files panel as reusable assets after your team runs the backfill.
* Publisher practice now saves the proposal and its private practice plan together. If the practice conversation changes while a proposal is being created or extended, no partial proposal is retained and the seller can retry from the current conversation.
## 5.558.0 — September 18, 2026 at 3:34 AM UTC
* Sales Agent providers can complete the confirmed no-spend media-buy step before certification or Storefront publication setup is complete. The exception is limited to the temporary certification source; live transactions remain gated.
* Buyers enrolled in the Creative Engines alpha can find a versioned guide that helps their agent create and refine campaign creatives without losing the output they selected or approved. The guide covers secure connection, advertiser mapping, the funding that applies to the request, named-parent refinement, exact-output approval and recovery after an interrupted request.
* Using the guide requires its two Creative Session tools, a supported engine capability and connection, any locked reference required by the selected engine, funding and an authorised quote before generation can be dispatched. It does not choose funding or prices, approve a creative for a seller, or launch a campaign.
## 5.557.1 — September 18, 2026 at 2:55 AM UTC
* Buyer agents now see that Creative search requires exactly one advertiser or campaign owner before calling the tool. Successful searches repeat the effective scope and filters in text and structured output, including the three promoted filter meanings: omitted for all visible results, true for the reusable shelf, and false for non-promoted results.
## 5.557.0 — September 18, 2026 at 2:49 AM UTC
* Completed Creative uploads now leave a durable conversation receipt in Claude, ChatGPT, and Interchange with the available saved metadata and verified video facts. Advertiser uploads include their reusable Library asset ID. A legacy provider upload without one includes its digest only as byte identity for the receipt, not as a value that can be saved, adopted, or attached later. The receipt does not create or attach a Creative, sync data, contact a provider, or deliver media, and the upload Task stays open for retry if the host cannot record it. Existing assets need no changes.
* No changes are required for existing V3 `save_creative` callers: omitting `mode` preserves complete-save behavior.
* You can now save a partial advertiser-scoped Creative with `mode: "draft"`, then replace its entire submitted authored and slot state by sending the Creative's current `stateRevision` as `expectedRevision`. Use `mode: "complete"` on the same Creative once every required media, authored, and repeat-group slot validates. Creative reads expose `state` and the authoritative whole-state `stateRevision`, while the existing opaque preview `revision` remains compatible and revision-bound previews continue to use the Interchange render frame. These advertiser-scoped draft and completion saves do not attach the Creative to a campaign or contact a provider.
## 5.556.4 — September 18, 2026 at 2:19 AM UTC
* Opening reviewer access now recovers automatically when an earlier login setup attempt left cleanup unfinished.
## 5.556.1 — September 18, 2026 at 1:22 AM UTC
* When you choose another Murph conversation while an earlier one is still loading, Murph now keeps the conversation you chose last.
## 5.555.0 — September 17, 2026 at 11:41 PM UTC
* When released, buyers already enrolled in the Creative Engines alpha will be able to save a campaign creative brief, generate variants, refine a named variant with feedback, and finalise the exact output they approve through V3 MCP.
* To use these actions, save a brief with `save_creative_session`, then explicitly request generation with `generate_variants`. Saving does not start generation. A saved engine connection and the existing account access are required. If a generation request is uncertain, recovery checks the original request without silently starting a replacement. Provider account and funding requirements still apply; finalising creative does not approve it for a seller or launch a campaign.
## 5.554.1 — September 17, 2026 at 11:16 PM UTC
* Fixed TikTok video creatives failing to sync to campaigns. Uploaded videos were being rejected every time due to a response-parsing bug in how we read TikTok's upload confirmation; video creatives now sync and attach correctly.
## 5.554.0 — September 17, 2026 at 11:10 PM UTC
* The Creative Library's Files and compose panel now loads the product picker in the Interchange web app. Previously the panel showed "Products unavailable" there, so you could not choose a product or its canonical formats; in Claude and ChatGPT the picker already worked.
## 5.553.3 — September 17, 2026 at 10:32 PM UTC
* Seller admins can open newly provisioned directory reviewer logins without a Secret Manager error.
## 5.553.0 — September 17, 2026 at 9:15 PM UTC
* When an MCP host cannot show the creative upload task, its fallback now opens the selected advertiser's Creative Library and starts the existing upload task. You do not need to select or create a campaign first.
* Additional account creation now appears only when your organization's accepted package includes an unused account slot. Otherwise, the account selector does not show a creation or upgrade control, and API requests fail without creating a partial account. Existing accounts are unchanged. Buyer accounts now ask for the **Operator domain**. Seller accounts ask for the **Company domain** and distinguish it from the public listing domain.
## 5.552.0 — September 17, 2026 at 8:59 PM UTC
* Buyer agents can pause an active media buy or resume a paused one through `save_media_buy`. Media-buy reads can also return the authoritative selected-product details and a fresh live AdCP status when requested.
* `get_media_buy` now includes `proposalId` in `pendingChange` when the queued change is waiting on seller approval through the async approval flow. Use this ID with `DELETE /api/v2/buyer/update-proposals/{proposalId}` to cancel the pending proposal and unblock a subsequent update.
## 5.550.0 — September 17, 2026 at 8:08 PM UTC
* Design-system widgets now render in the same brand typeface as the rest of the app. Headings sit slightly lighter than before, because that typeface ships Regular and Medium only and no longer synthesises a heavier weight.
* Creative Library now shows collections as a navigable tree with nested paths. Dragging a creative onto a collection adds it, with Undo available.
## 5.549.1 — September 17, 2026 at 7:46 PM UTC
* Seller admins can now open reviewer login credentials from eligible child accounts.
## 5.549.0 — September 17, 2026 at 7:20 PM UTC
* Selected sellers can choose **Try as a buyer** in the Merchandising Agent interview to practise a buyer conversation. Coach a completed reply, write and test a private lesson, then review it before applying it to your Playbook. Practice uses your normal allowances; it books no media and does not open advertiser admission. Existing conversations need no changes.
## 5.547.0 — September 17, 2026 at 6:07 PM UTC
* The Creative Library now has an advertiser-scoped Assets view for browsing reusable image, video, and audio files. Image and video assets can be selected for Creative assembly; audio remains browse-only for now.
* When a published Rate Card is available for your Free Seller Account, Interchange prompts you to review it from any app page after the Terms of Service step. An organization administrator opens the existing plan review and explicitly confirms the exact offer before accepting. Other members see an **Ask your admin** message. You can decide later or continue without a paid plan, and return through **Plan & Billing** at any time.
* Plan & Billing now shows only the public IU plans that cover every active Buyer, Seller, and Partner product in your billing organization. Seller choices also follow the Storefront operating mode. Existing accepted plans stay in effect, so there is nothing to change unless your organization chooses a new plan.
## 5.546.0 — September 17, 2026 at 5:23 PM UTC
* Product discovery now polls a successful provisional result without sending internal routing fields that the buyer API rejects.
## 5.544.0 — September 17, 2026 at 4:08 PM UTC
* Report progress while a `chat` turn runs. A caller that sends a progress token now receives `notifications/progress` updates as the turn works, written in the language the conversation is already being answered in, so a long analysis is no longer indistinguishable from a hung request. The first update goes out before the turn starts: preparing a caller's status language never delays it by more than a second, and cancelling the request releases that wait immediately. Callers that send no progress token keep the existing synchronous behaviour.
* The Teach opening now explains how your materials prepare your agent and how to test it safely with a practice brief. It presents Teach as the one clear way to begin.
* Allow existing active reviewer grants to open the reusable directory reviewer login.
* Practice proposals now create a new attempt from the same brief when you select Run again, then show the new response beside the previous one. Products, prices, uploads, and saved feedback are unchanged.
* Creative sync now reaches sellers that publish legacy creative formats when a creative is pinned to one of the product's format options. The selected option is sent back as the exact seller format it came from, so buyers no longer need to retry the media buy without syncing creatives first.
## 5.543.0 — September 17, 2026 at 2:37 PM UTC
* Sellers can upload a private file from Product Marketing in Murph without the file being rejected after selection. Existing file permissions and the general Murph attachment flow are unchanged; sellers do not need to take action.
## 5.542.0 — September 17, 2026 at 1:10 PM UTC
* Product searches that take 30 seconds now preserve their in-progress results and continue the same search instead of stopping before products can be returned. Nothing needs to change on your side.
* Buyer Pages now load reliably in external MCP Apps hosts and provide a clear recovery message when Page authorisation cannot be verified.
* Media buys now reach sellers that publish their creative formats in the legacy form, instead of failing with "the selected seller product did not provide one unambiguous legacy format reference". When you pin a creative to a format option a seller's product advertises, that pin now routes to the exact seller format it was built from. Nothing about how you select a format changes, and the affected buys can simply be retried.
* Sellers can open and reuse reviewer test credentials for their public listings.
* Buyers can organise advertiser creative collections into nested groups, see each collection's parent and distinct subtree member count, and archive a collection after its active children are archived.
* Partner organizations with an issued private offer can review its terms in Plan & Billing and continue to secure payment setup when required.
* FreeWheel sellers can now build products from direct Ad Units, Ad Unit Packages, or both. Product creation follows each source's inventory selection rules, so networks without packages can create valid drafts without a separate seller setting. Existing products are unchanged.
## 5.541.0 — September 17, 2026 at 11:36 AM UTC
* Enrolled buyers can now upload WAV and MP3 files up to 50 MB through the V3 creative upload Task. After inspection, the file is saved to the advertiser's creative library and can be found with `search(kind: "creative_asset")`.
* These audio assets are not yet available to `save_creative`, and uploading does not create or deliver a Creative. Existing image, MP4, and connected-provider audio workflows continue unchanged. Buyers who are not enrolled see no change.
## 5.539.1 — September 17, 2026 at 10:32 AM UTC
* If Creative Engines is enabled for your Buyer Account, open **Creative Engines** under **Discover** in Murph, or ask Murph to find a registered provider and open its setup on the Page. Start the secure connection from the Page. The form names the provider and, for OpenAI connections using your own key, links to the OpenAI API key dashboard. Creative Engines remains an alpha; this change does not make generation available.
## 5.539.0 — September 17, 2026 at 10:17 AM UTC
* Prepare remote MCP directory reviews from Listing. Request read-only reviewer access and reopen the same email and password for OpenAI, Anthropic, Microsoft, and other directories until you revoke it or it expires.
## 5.538.0 — September 17, 2026 at 9:58 AM UTC
* To cancel a campaign through `/mcp/v3`, first cancel or settle every executable or unsettled media buy. Then call `save_campaign` with `desiredPhase: "canceled"` and the campaign's current revision.
* Campaign cancellation is terminal and does not cancel media buys for you. If any executable or unsettled media buys remain, `save_campaign` names them and leaves the campaign unchanged. Archive is a separate visibility action; this cancellation request does not archive the campaign.
* When you open Library in Murph, your documents and pages or slides now load correctly. An empty Library shows its first-run guide, and a failed read shows the full retry action. Nothing to change on your side.
## 5.537.0 — September 17, 2026 at 9:19 AM UTC
* Demo Storefront sellers can now use Teach to review and correct extracted candidates before opening a frozen synthetic evaluation in Proposal Pass. The evaluation keeps its sources and limits visible, including a synthetic rate card for planning only, not a live or buyer-quotable price. Custom fulfilment stays a named human or Connect follow-up; it is not treated as available automatically.
## 5.536.1 — September 17, 2026 at 8:39 AM UTC
* Embedded sales agent 1.43.4: declare AdsWizz hosted audio durations, quarantine partial AdsWizz creates, read the AdsWizz agency currency from the ad server, respect AdsWizz delivery in budget updates.
## 5.536.0 — September 17, 2026 at 8:32 AM UTC
* Ask Murph to show your creatives and open a portable Creative Library for one advertiser. The Page clearly separates all visible Creatives from the promoted reuse shelf, keeps active filters and incomplete results visible, and opens revision-bound previews without exposing another advertiser's records. Existing `search(kind: "creative")` calls continue to return plain objects.
## 5.535.0 — September 17, 2026 at 7:38 AM UTC
* Sales Agent providers can run a zero-spend creative check that records a separate result for the exact implementation revision after cleanup completes.
## 5.533.0 — September 17, 2026 at 5:35 AM UTC
* Sales Agent providers can explicitly confirm each no-spend sandbox transaction step in a run-scoped technical certification check before any client setup.
* RFP proposal-file rendering continues after the request that queued it ends.
## 5.531.0 — September 17, 2026 at 2:05 AM UTC
* Sales Agent providers can start a no-spend brief-only technical certification check with a dedicated, run-scoped fixture before connecting an Inventory Source.
## 5.530.0 — September 17, 2026 at 1:10 AM UTC
* When you open an active Demo Storefront inventory source in Murph, its Display and CTV readiness now loads without an Enterprise Merchandising plan. Creating or changing custom modular inventory sources still requires Enterprise access. Nothing to change on your side.
## 5.529.0 — September 16, 2026 at 11:31 PM UTC
* Seller Material decks now produce slide previews, OCR, and semantic document inspection in production. The renderer runs inside the private worker boundary and does not receive seller or cloud credentials.
* A media-buy change that every publisher rejects for a reason that cannot be fixed by sending it again — an invalid request, or an outright refusal — now stops immediately and reports why, instead of being re-sent every two minutes for six hours and then going quiet.
* Nothing about a valid change is different, and a change rejected for a reason that might clear on its own — a publisher's system being briefly unavailable, or an access grant that has not landed yet — is still retried for the usual six hours. Publishers see the same "forwarding failed" signal they already receive, once, rather than repeatedly.
* Cancellations are deliberately unaffected: a cancellation that fails to reach the publisher stays retryable, because a live campaign must never be recorded as stopped while it is still delivering.
## 5.528.0 — September 16, 2026 at 10:09 PM UTC
* Expose direct parameter types beside reusable MCP schema references so Anthropic's directory scanner can validate the live tool catalog. Remove the non-actionable Claude plugin-package checkpoint from the storefront distribution flow and lead publishers directly to connector submission.
* Behind the existing default-off `organization-settings-in-place` flag, corrected the server-side authority foundation for the organisation-posture endpoint and added the B1 epoch proof surface:
* The flag is now evaluated for every caller before any posture is returned, including excluded principals. An excluded principal whose parent org is enrolled receives 200 `ineligible` rather than 403.
* Standalone posture is restricted to valid PARENT or STANDALONE customers with no parent; unknown or malformed topology is ineligible after the flag check.
* The `OrganizationPostureService` production entry point wires all dependencies (Prisma, Agentic DB, PostHog, delegation reader) inside the service module; the route only builds context and calls one method.
* `BillingAccountService.getInheritedBillingStub` uses a neutral locked Core FOR SHARE substrate: topology is read with `FOR SHARE`, and while those locks are held a single Agentic `REPEATABLE READ` snapshot evaluates sponsorship, membership, and delegation. Concurrent reparenting cannot produce a stale parent identity and the billing result is fully assembled before Core releases. Billing has no feature-flag gate and no epoch computation.
* Shared Zod runtime contract (`OrganizationPostureKindSchema`, `OrganizationPostureResponseSchema`, `OrganizationAuthorityProjectionSchema`) exported from `@scope3/agentic-contracts/organization-settings`.
* Non-impersonated platform SUPER\_ADMIN principals authenticated to a CHILD account receive `direct_parent_admin` without a parent membership read.
* **B1 addition:** `GET /api/v2/accounts/organization-posture` now includes required `authorityCustomerId` (integer — the authenticated account whose authority was resolved) and `authorityEpoch` (opaque 16-hex-char string) fields in every 200 response. The epoch encodes all verified authority-bearing inputs (principal identity, credential, customer topology, parent membership, child projection, and delegation) and rotates whenever any of these change. The `/auth/me` response optionally includes the same `authorityCustomerId`+`authorityEpoch` pair for enrolled accounts. Both endpoints derive the epoch from the same authority-proof resolver so the values are guaranteed to match for the same canonical authority state. A UI hook (`useOrganizationPosture`) is shipped in this slice but has no consumers; it is wired to the posture surface exclusively and carries no lifecycle dispatch, route decisions, redirects, or rollout logic.
* Creative Library and Catalog feeds now follow your interface language in German, Spanish, French, Italian, Japanese, Korean, Dutch, Brazilian Portuguese, and Simplified Chinese. Names and content from your account remain unchanged. No action is required.
## 5.525.0 — September 16, 2026 at 8:42 PM UTC
* An approved media buy now has its flight start re-checked at the moment it is sent to the seller, instead of only when it was first submitted. A start that has since passed is sent as "start now" rather than as a stale timestamp the seller would reject.
* A media buy whose flight has already ended is no longer sent at all. Previously it was retried against the seller every two minutes for six hours, rejected every time, and then went quiet — leaving the buy approved but never placed, with nothing to explain why. It now stops immediately and reports `flight_window_elapsed`, so buyers and publishers both see a stated reason and can submit a replacement buy with a current flight window.
* Buys with a valid flight window are unaffected.
## 5.524.0 — September 16, 2026 at 8:17 PM UTC
* The Creative Library composer now lets you choose an eligible product by seller and product name before selecting its creative format. Incomplete seller results remain visible and selectable, with clear options to continue or retry the browse, and saves and previews retain the selected format exactly; there is nothing to change on your side.
* Executing a staged media-buy batch no longer repeats an already-completed campaign link, preventing an otherwise valid batch from failing during concurrent activity.
* Buyer Accounts enrolled in the Creative Engines preview can now browse registered engines and manage provider connections, account selection, and advertiser mappings. Declared support and observed account access are shown separately. Provider prices and generation are not available in this preview.
## 5.523.0 — September 16, 2026 at 7:29 PM UTC
* Fixed searchable dropdowns being cut off inside dialogs. In the Create account dialog, the option list for a field like additional payment currencies or support channel was clipped at the edge of the dialog's scrolling area, so options below the cut were invisible and unreachable. The list now opens over the dialog, scrolls with a trackpad, flips above the field when there is no room below, and closes on Escape without also closing the dialog.
* Buyer agents can now browse an advertiser's visible wholesale products across eligible Sellers with `search(kind: "wholesale_product")` before a campaign exists. Pass `filter.advertiserId`; bounded results include the exact product and Seller IDs needed for `creative_format`, and incomplete Seller reads or withheld unsafe identities are reported instead of appearing as an empty catalogue. Seller product search is unchanged.
## 5.522.0 — September 16, 2026 at 7:01 PM UTC
* The notification bell now shows a dot when you have something unread, instead of a number. The exact count lives in the panel the bell opens, so the bar just tells you there is something waiting. The help, notifications, switch-account and menu icons beside it are quieter too — they carry no background until you hover them, so the top bar reads as one row of icons rather than a row of filled buttons.
## 5.521.0 — September 16, 2026 at 6:35 PM UTC
* In `/mcp/v3`, buyers can now set a media buy's flight and budget when `save_media_buy` stages products or accepts a proposal. A total budget applies to a single-product buy; use per-product budgets for a multi-product buy. Flight dates use ISO 8601 datetimes; use a future start time or `asap` to start immediately. Calls that omit these new inputs continue to work unchanged.
## 5.520.2 — September 16, 2026 at 6:10 PM UTC
* Inventory-source and other seller workspaces now open at their content height in chat instead of taking over the conversation window when their content is short. Tall workspaces remain capped and scrollable, and the surrounding conversation stays accessible.
## 5.520.0 — September 16, 2026 at 5:18 PM UTC
* Creatives that name only a format kind now attach to products that declare the same format more than once. Some catalogues list one size under two option ids; that counted as two competing options and the product dropped the creative. Identical declarations are now treated as the one format they describe. Creatives pinned to a specific option are unaffected, and no creative is sent to a seller with an option id we had to guess at. The message shown when a creative genuinely cannot attach now counts the options on the product it failed against, rather than every option on the buy.
* Campaign goals are now opt-in. You can clear a goal without recreating the campaign.
## 5.519.0 — September 16, 2026 at 4:51 PM UTC
* Brand logos no longer run to the edge of the tile they sit on. A logo is now centred in the avatar's content slot, so the brand colour behind it reads around the mark and a list of advertisers lines up row to row whatever shape each logo is — a square icon, a wide wordmark, and a logo that carries its own background all render the same size.
* The colour behind a logo is now chosen by looking at the artwork itself rather than by trusting what the brand kit says about it, which is often missing. A white knockout mark gets a dark backing and a dark mark gets a light one, so a logo can no longer disappear into its own tile. An advertiser with no logo and no brand kit falls back to its initials.
* The advertiser-list response no longer carries `logoFill`. It said whether a logo should fill its tile, and nothing fills a tile any more.
* Fix two agent-facing text issues in the v3 buyer surface.
* Currency lock: the skill now explicitly states that an advertiser's `primaryCurrency` is freely updatable, while a media buy's settlement currency is a separate value locked at creation and cannot be changed. The currency mismatch error message in `save_media_buy` has been updated to reflect this correctly.
* Draft wording: agent-facing prose in `save_media_buy` and the buyer skill now uses "draft" instead of "staged/staging" to match the product vocabulary. Status enum values, field names, and wire identifiers are unchanged.
* V2 Buyer connectors now publish a smaller tool catalogue so supported AI clients can load it. Existing API inputs and validation do not change, and buyers do not need to take action.
## 5.518.0 — September 16, 2026 at 4:32 PM UTC
* Optimization goals (`performanceConfig`) are now hints you can attach to any campaign — they no longer transfer composition ownership to the platform optimizer. Composition is now controlled by `autonomy.inventorySelection`.
* **Before:** setting optimization goals made a campaign `performance`, which blocked manual seller picks and `create_media_buys` calls from staging.
* **After:** `autonomy.inventorySelection` owns composition:
* `manual` (default) — you pick sellers and products directly
* `propose` — the platform drafts a selection for your approval
* `automatic` — the platform optimizer owns product selection; manual staging is rejected
* Campaigns with goals but without `inventorySelection: "automatic"` can now stage manual media buys. To use the platform optimizer, set `autonomy.inventorySelection: "automatic"` alongside your goals.
* **Existing performance campaigns** are automatically migrated: any campaign with optimization goals and no explicit `inventorySelection` receives `inventorySelection: "automatic"`, preserving their current behaviour.
* The `mode` field on campaign reads continues to return `performance` when goals are present, `discovery` otherwise — v2 consumers see no change on reads.
* Creative Library now confirms that the upload task opened before showing success, and shows a clear error when the task cannot open.
* OpenAI package downloads no longer leave a stale preparation message after the browser starts the download.
* Fix async task poll never firing for third-party seller sources (e.g. PubX) when the seller responds in the final 2 seconds of the discovery budget.
* The SDK call was being aborted by an internal deadline signal 2 seconds before the outer timeout, cutting the connection before the response arrived. Interchange never received the async task ID, so no poll row was created and `tasks_get` was never called. This change gives sellers the full discovery budget to respond, and adds a background flush to recover the task handle if the flush-to-DB races with the deadline.
* Seller identity labels in the v3 buyer surface now use `storefrontId` instead of `sellerId`, making it clear the id refers to a publisher storefront rather than an internal account. Searches that reference a seller by name now also match on platform id and publisher domain. When a storefront's display name disagrees with its publisher domain (for example, a name of "Daily Mail Digital" paired with a domain of "wonderstruck.org"), the domain is shown instead — catching data mismatches at read time without requiring a data correction.
* The legacy `connection.customerId` remains available for one compatibility release and is now deprecated. Buyer integrations should move to `sellerId` or `storefrontId`; we will remove `connection.customerId` in a follow-up release after the transition period.
## 5.517.0 — September 16, 2026 at 3:46 PM UTC
* When one inventory-source health change also updates Storefront liveness, you now receive one source-health email instead of a second Storefront update for the same change. The Storefront state change remains in your in-app notification history. Nothing to change on your side.
* Buyers receive an incomplete, pollable product-discovery result when a background publication misses the request deadline, instead of an error that requires starting over.
* The V3 buyer agent now requires explicit user confirmation before staging a media buy, even after a proposal round completes. Previously the agent could proceed to `save_media_buy` based on a prior direction or conversational momentum without waiting for an active "yes". After this fix, the agent stops after presenting products or proposals, confirms the exact commitment with the user, and calls `save_media_buy` only after receiving explicit approval in the current exchange. The agent also treats any failure or error in the buy flow as a fresh decision point — a "try again" re-proposes and confirms rather than silently re-authorizing the prior action.
* Buyer agents can search for and open Sellers whose activation is deferred without receiving a conflict response. No action is needed.
* Parent organizations can now see each child account's attributed customer IU usage for the current billing period in Plan & Billing. Accounts with no rated activity show 0 IUs; unavailable or unratified usage remains blank.
## 5.516.0 — September 16, 2026 at 3:06 PM UTC
* OpenAI package downloads now start from a direct click after preparation, so browsers no longer block them as pop-ups.
## 5.515.2 — September 16, 2026 at 2:22 PM UTC
* Connections can open the advertiser setup flow from the buyer workspace.
## 5.515.0 — September 16, 2026 at 1:41 PM UTC
* Creative Library uploads can now include searchable tags. After an upload finishes, the agent receives the saved asset's name, tags, file details, and verified video facts so it can accurately confirm the result and continue building a Creative. Existing uploads need no changes.
## 5.514.1 — September 16, 2026 at 1:04 PM UTC
* Plan & Billing now opens inside Account settings, keeps its selected tab in the URL, and sends older billing links to the same in-frame destination.
## 5.514.0 — September 16, 2026 at 12:38 PM UTC
* Interview actions now stay unavailable until their matching workflow is ready, instead of starting a general chat.
## 5.513.0 — September 16, 2026 at 11:59 AM UTC
* Fixed two bugs that could leave a creative attached to a Meta media buy without ever reaching Meta, or reaching it with no explanation of why it failed:
* A creative attached to a media buy before launch was never recorded against its package, so buyers and their agents saw 0 creatives linked even though the creative was correctly associated internally.
* When a background retry to re-sync a creative to Meta failed, the failure reason was sometimes left blank instead of explaining what went wrong, making it impossible to tell why delivery hadn't started.
* Standalone accounts no longer show a duplicate organization settings entry, and Buyers without another account or account action no longer see a redundant selector.
## 5.511.4 — September 16, 2026 at 11:22 AM UTC
* OpenAI package downloads now revalidate the current approved listing before generating the archive.
## 5.511.3 — September 16, 2026 at 10:39 AM UTC
* Let buyers assemble uploaded Library assets with `save_creative` by passing the exact `creativeFormatId` returned by Creative format search or read.
* You can now upload supported seller material from the Product Marketing Teach page. The page sends the file to private storage and starts processing it; pasted text and existing material are unchanged.
## 5.511.1 — September 16, 2026 at 9:57 AM UTC
* Package budget updates now return success only after the seller confirms the change. If every seller proves that a rejected update left no upstream mutation, the existing live budget remains in place and the rejected proposal no longer appears as pending approval. Asynchronous and otherwise uncertain outcomes stay pending for reconciliation.
* The Campaigns page now loads self-serve advertiser campaigns correctly. Nothing changes for existing campaign filters.
## 5.510.0 — September 16, 2026 at 9:02 AM UTC
* Proposal passes now show buyers—and the sellers reviewing them—when a request depends on unsupported attribution or human-fulfilled custom work, so neither side mistakes a limitation for an automated promise.
* Make the current Proposal Pass response the visual focus, show sellers a direct before-and-after comparison after coaching without hiding preserved commercial limits, and keep format previews refreshing reliably while proposal files render.
## 5.509.1 — September 16, 2026 at 7:17 AM UTC
* Fixed a bug where a creative could stay stuck in a failed/pending-approval state on a Meta media buy after activation, because the background job that re-syncs assigned creatives to the seller wasn't resolving the format details some Meta placements require. Retrying creative delivery after a media buy goes live now carries the same format information as the initial sync.
## 5.508.0 — September 16, 2026 at 4:57 AM UTC
* Fixed an issue where MCP apps opened in Ask Murph could open without the original request details. Nothing to change on your side.
## 5.507.3 — September 16, 2026 at 3:24 AM UTC
* The Plan & Billing Accounts tab now shows intelligence usage by account. Account-level fees and billing delegation controls remain unavailable, and no action is required.
## 5.507.2 — September 16, 2026 at 3:03 AM UTC
* Open API calls from the Agents workspace's left navigation on both the buyer and seller sides. It opens the existing Activity widget in the main screen, with chat still available. The shortcut has moved out of Inventory and the general buyer navigation. API keys remain in Settings.
* The seller Agents collection now opens from a standard left-navigation row instead of appearing as a management widget inside the sidebar. Agent creation and selection stay in the main screen.
* Selecting an Agent groups its sections beneath its name. API calls stays separate and account-wide; selecting an Agent does not filter calls or API keys. On narrow screens, use All agents to return to the collection instead of an Agent dropdown.
* Existing access rules and Calls-beta eligibility are unchanged. Accounts outside the beta retain the Changes fallback. Sellers using the older navigation, or a failed-checklist fallback, keep their existing Activity shortcut. No credential or integration changes are needed.
## 5.507.1 — September 16, 2026 at 2:28 AM UTC
* Plan & Billing → Accounts no longer shows an empty Period fees column. Organization-level billing and account usage are unchanged; no action is needed.
## 5.507.0 — September 16, 2026 at 2:06 AM UTC
* Creative reads now identify the durable Library asset used to assemble each manifest asset. This keeps the reusable asset and Creative-specific manifest identities distinct while making their provenance navigable.
## 5.505.1 — September 16, 2026 at 12:58 AM UTC
* For buyers, REST responses continue to provide a numeric `file_size`. Verified video sizes include `file_size_status: "verified"`; when verification is unavailable, `file_size` is `0` and the status is absent instead of reporting the length of the URL. No buyer action is required.
* For sellers, sales-agent integrations now receive video size, container, and codec through video-specific properties. The existing generic `file_size` and `format` aliases remain during the compatibility transition, so current custom integrations continue to work. Unknown sizes are omitted from delivery and size checks under both field names, while older video records keep their available file-type details. No storefront setup change is required.
* Show a clear loading indicator while an OpenAI submission package finishes processing.
## 5.504.0 — September 15, 2026 at 10:29 PM UTC
* The per-buyer auto-approve Page has been retired. New clients should use the storefront-wide media-buy approval setting in AI Business Rules.
* The deprecated V2 REST and MCP operations keep their existing behaviour while clients migrate. Existing V2 buyer overrides therefore continue to apply during this window.
## 5.503.3 — September 15, 2026 at 10:18 PM UTC
* The Creative Library now accepts the exact Product ID returned by marketplace discovery and checks promoted MP4 container, codec, duration, and dimensions before save. New durable uploads also retain their original filename. After one advertiser-level save, buyers can preview the saved revision and find the Creative once under All creatives. No integration change is required, and no campaign or provider is contacted.
## 5.503.2 — September 15, 2026 at 10:09 PM UTC
* Source tests no longer blame your agent for a discovery that returned nothing conclusive.
* When you test an inventory source, we run a buyer discovery against it and then select products from the result. If that discovery came back empty, the test told you your source returned no products and suggested your inventory did not match the brief — even when the discovery had not finished, or when it reported no reason at all.
* A source test now says plainly when it is inconclusive. If the discovery had not finished, it says so and names which agents were still outstanding. If the discovery came back with no products, no error, and no explanation, it says it cannot tell a source with no matching inventory apart from a discovery that returned before the source answered. Either way it asks you to re-run rather than drawing a conclusion.
* It does not swing to the opposite claim either: an unfinished discovery is reported as inconclusive, not as our fault, because an agent that did not answer in time may have been slow itself. Where discovery genuinely finished and the source reported products, a skip reason, or an error, the test attributes it exactly as before.
* Every run also records whether discovery had completed when its products were read, so a past result can be re-examined instead of assumed.
* This changes what a source test reports, not how discovery runs or how long it waits.
## 5.502.1 — September 15, 2026 at 9:01 PM UTC
* When a selected creative format has one compatible asset slot, your agent can now save an advertiser-owned image or video without supplying a slot name. Formats with multiple compatible slots or repeatable groups still require an explicit slot, and any other required assets are unchanged. Nothing to change on your side.
## 5.500.0 — September 15, 2026 at 6:48 PM UTC
* The creative uploader now points to the correct **Close** action when it records an asset but cannot dismiss the upload card.
## 5.498.0 — September 15, 2026 at 6:09 PM UTC
* The menus in the top navigation bar — account, help, workspace picker, notifications, and the organization switcher — now use the new design system, so they match the look of the rest of the product. Opening Ask Murph from the navigation bar no longer reloads the whole app.
## 5.497.2 — September 15, 2026 at 4:12 PM UTC
* The Advertisers and Campaigns pages load again in the web app. Both pages could open to "Couldn't load advertisers" (or an empty campaign list) with a Retry button that never recovered, because the page discarded its own read authorization while starting up.
## 5.497.0 — September 15, 2026 at 2:58 PM UTC
* Buyers can now select **Show preview** for a saved Creative in the Library. V3 `get` clients can request the same short-lived, revision-bound preview with `include: ["preview"]`. Direct image, video, and audio sources are renderable; active tag documents return an honest reason rather than exposing nested provider resources or trackers. V3 Creative reads no longer forward the legacy provider preview URL or tracker fields; use the capability-gated preview instead.
## 5.495.3 — September 15, 2026 at 2:27 PM UTC
* First-time users who return to signup after signing in with an existing password can now continue account setup. If the recovery session has expired or is not eligible, signup shows an error instead of leaving them on the same step. No action is needed unless you encountered this issue; sign in again to retry.
* FreeWheel sellers can now set the execution currency in Inventory Sources without re-entering or replacing source credentials. Existing sources without one must add the currency FreeWheel uses for insertion orders, and it must be one the storefront settles.
## 5.494.0 — September 15, 2026 at 12:06 PM UTC
* Storefront operators with an approved, expiring lifecycle-test exception can commit the exact test availability row while their Storefront is paused. The row stays out of buyer catalogs and does not count as publisher authorization or activation evidence.
## 5.492.0 — September 15, 2026 at 11:16 AM UTC
* Buyers can now assemble advertiser-owned image or MP4 assets into a canonical creative format directly from the Creative Library, then save and verify the result without attaching it to a campaign.
## 5.491.0 — September 15, 2026 at 10:33 AM UTC
* In creative approvals, live previews now stay closed until you choose Show preview and appear in their placement format. Expired or superseded previews do not load; refresh the queue for a current preview. External creative links continue to open separately.
## 5.489.0 — September 15, 2026 at 7:51 AM UTC
* Buyer agents can now read the exact creative format and slot requirements for visible products before a campaign exists. Use V3 `search` and `get` with kind `creative_format`, then pass the returned `formatOptionRef` unchanged when saving the creative. The read makes no provider call or setup request. Seller-owned product declarations are available without a provider connection; direct-adapter products keep their existing active-connection visibility rules.
## 5.488.1 — September 15, 2026 at 7:00 AM UTC
* Buyer Library MP4 Creatives can now reach a compatible selected Meta destination when they are attached to a campaign. Interchange rechecks the current buyer, advertiser, account authorization, Library source, format, and revocation state before Meta delivery, without another upload or a private storage URL.
## 5.486.2 — September 15, 2026 at 5:42 AM UTC
* HTML5 ZIP uploads in the V2 buyer flow now accept one enclosing creative folder and report missing or ambiguous banner dimensions explicitly instead of calling a readable archive corrupt. Hidden export metadata no longer creates extra creatives in the campaign upload UI. Mac metadata is removed from delivered HTML5 ZIPs while creative files and relative paths are preserved. V2 Murph can retrieve saved attachments on a later turn before inspecting them. HTML5 adoption through the V3 connector remains unavailable.
## 5.486.0 — September 15, 2026 at 4:34 AM UTC
* An empty sales Library now explains which documents to add and opens the existing private file chooser from the drop area. Loading, failed reads, filtered results and narrower empty views keep their existing messages.
## 5.485.0 — September 15, 2026 at 3:47 AM UTC
* AI Business Rules now keeps a support correlation ID visible when a policy save fails without a usable server response.
* Buyers enrolled in the Creative Engines preview can list creative sessions for a campaign and open them through V3 `search` and `get`. Each session includes its persisted revision, plan, variants and evaluation, subject to current campaign and advertiser access. Session reads do not start generation. V3 session writes and `generate_variants` are not available in the preview.
* Separately, existing V2 creative-session fixes take effect when this release is deployed. They preserve explicit image dimensions and an AudioStack duration, refuse a selected credential when it is unavailable rather than falling back to another key, and return the saved result when a finalized session is retried with the same selected variant, name and message. A batch whose direction labels resolve to the same provider key is rejected before generation. The Creative guide explains retry behaviour and direction-label requirements.
## 5.484.0 — September 15, 2026 at 3:10 AM UTC
* Library documents you upload now get faithful page and slide previews: PDF pages and PowerPoint slides render as they look, and each page's inspection state comes from that render rather than a text-only scan. If a page cannot be rendered fully, the Library says so instead of presenting it as ready. Nothing to change on your side; documents already in your Library keep their current previews until they are reprocessed.
## 5.482.1 — September 15, 2026 at 1:40 AM UTC
* Campaigns opened with an existing relationship or status filter now load the matching list. Invalid filter values and extra filter fields remain rejected.
## 5.482.0 — September 15, 2026 at 12:49 AM UTC
* Advertiser Creative Library can switch the same loaded results between Grid and List while keeping filters and detail context, and can manage advertiser-owned collections. Refreshes and collection results stay scoped to the current advertiser.
* If your seller account uses a third-party sales agent, Source Diagnostics now shows Storefront availability, Agent certification, and Source health separately. It gives you the next action for the status that needs attention. This release only clarifies existing status; it does not change whether your storefront can transact.
## 5.481.1 — September 14, 2026 at 11:26 PM UTC
* The Advertisers Page now loads its advertiser list reliably in MCP v3 hosts. Nothing to change on your side.
## 5.481.0 — September 14, 2026 at 11:00 PM UTC
* Browse Library documents and decks with source-grounded previews, file types, and relevance labels. Search and grouping use the canonical, seller-scoped Material read projection while keeping source mentions, catalog links, suggestions, and seller confirmations distinct.
## 5.480.0 — September 14, 2026 at 10:34 PM UTC
* Promote finalized image and video uploads into durable advertiser asset records, expose them through V3 search/get, and let `save_creative` assemble reusable `assetId` inputs.
* Seller decision details now leave buyer advertiser and buying-operator identity unknown when a captured brief request omits them, instead of presenting compose-run fallback data as buyer-declared identity.
* Start storefront brief tests from Storefront Briefs. Test Runs no longer appears in navigation or starts new runs; historical links still open during the rollback window, and **Fix this run** remains available for failed or stalled records.
* Provisioned Demo Storefront accounts can open their Demo Storefront page from the setup rail without modular source access.
## 5.478.0 — September 14, 2026 at 9:25 PM UTC
* Advertiser-owned creative collections can now be read and changed through the existing creative collection tools. Changes require the current collection revision, support name or description and saved-creative member changes, and do not attach a campaign or add executable behavior.
* Creative collection search now accepts an explicit advertiser scope, and `save_creative_collection` can create an advertiser-owned non-executable collection without attaching it to a campaign or changing membership.
* When a seller investigates a media-buy mutation that did not go out, Source Diagnostics and Murph now show the intended mutation, any capability check that did run, and whether Interchange stopped before sending the mutation. The view identifies Interchange as the owner of that local stop and directs the seller to the recorded evidence while Interchange resolves the local setup failure and retries through the normal operation path; it does not ask the seller to change their Agent or Source configuration.
* Opening a seller from the connections widget now shows a tabbed detail in place — Connections & advertisers and Controls — instead of a side drawer. It lays out your media partners, the accounts each connection authorizes, and the advertisers mapped to them, with icon statuses (hover for detail) and inline actions; the advertiser search now filters whichever tab you're on. Section and tab labels are clearer too: Media Partners, Connections, and Controls.
## 5.477.3 — September 14, 2026 at 8:51 PM UTC
* New signups no longer inherit a retired Seller account or its billing family from an email-domain match. Child accounts and archived Seller accounts now require an explicit invitation or support-led recovery. Sign-in fallback also ignores historical permissions on inactive accounts. No action is needed for active standalone accounts.
## 5.476.0 — September 14, 2026 at 7:20 PM UTC
* Inventory Sources now distinguishes a healthy ad-server connection from whether that source can accept a new buy. If execution currency blocks buying, the page names the blocker and keeps seller-owned actions separate from work Scope3 must complete instead of saying that nothing needs attention.
* Show seller-facing Core reporting capability, exact Source attribution and health, and retained obligation/revision evidence without treating one Source as Agent-wide health.
## 5.475.0 — September 14, 2026 at 6:19 PM UTC
* Library Material search and lookup now show source file type separately from business format tags, and keep topics, audiences, product relationships, suggestions, and seller-confirmed applicability distinct.
* Material `get` can now include `repeats` to show separate readable units with the same complete rendered preview in one rendering configuration. Each unit keeps its own source, rendition, and place in the document. Results are bounded and may be incomplete. This comparison does not mean the Material has the same meaning or is an approved template, and seeing a result does not make it available for model use or reuse.
* Library Material searches can return indexed evidence matches from current readable source units, with source and inspection identities but no source text.
## 5.474.2 — September 14, 2026 at 5:55 PM UTC
* Run transaction validation from **Required to go live** now opens the exact Agent and inventory source that needs proof. If Interchange cannot identify one active Agent connection for the inventory source, it explains whether to connect the source or reconcile its connections instead of reopening Seller Setup.
## 5.474.0 — September 14, 2026 at 5:19 PM UTC
* v3 `get_delivery` seller delivery responses now include two new fields: `period` (the resolved reporting window — `start` and `end` dates) and `totals` (an additive rollup of impression and click metrics across all matched rows). Non-monetary metrics such as impressions and clicks are available in `totals`; monetary metrics are unavailable because seller delivery data does not carry currency. No changes to existing calls are required — the new fields appear alongside the existing response without breaking current usage.
## 5.469.1 — September 14, 2026 at 2:26 PM UTC
* Murph chat no longer blocks new messages when it crosses the 24-hour token threshold. Gemini-backed chats also avoid repeating expanded tool instructions between steps, reducing token use. The runaway-use safety backstop is unchanged.
## 5.469.0 — September 14, 2026 at 2:05 PM UTC
* Seller RFP responses and proposal files now begin processing when they are queued instead of waiting for the next periodic worker check.
## 5.468.0 — September 14, 2026 at 1:06 PM UTC
* Inventory Components now keeps the publisher-property coverage option you choose when it prepares a product for review. Coverage for all publisher properties and for tagged properties is sent exactly as your source provides it.
## 5.467.0 — September 14, 2026 at 12:36 PM UTC
* Inventory Components now asks you to choose the publisher-property coverage for a draft product. A single source-provided option is selected automatically; when several are available, choose the one that covers the product before review. The product check no longer adds a contradictory advisory when the ad server already reports missing coverage as a blocking issue.
* Connections now waits for its owner-authorised Page session before loading or changing seller connections, and lets the user retry a failed launch.
## 5.466.0 — September 14, 2026 at 11:34 AM UTC
* Buyers can now assemble finalized image and video uploads into an advertiser Creative before a campaign exists. Campaign-scoped saves still create and attach in one operation.
## 5.465.0 — September 14, 2026 at 11:13 AM UTC
* Inventory Components now loads each product’s complete editable definition before you save. Review and update canonical formats and their parameters, placements and inventory, targeting, video placement types, channels, and every pricing option, while source-managed fields remain visibly read-only. The editor validates complete and repaired products and safely patches only your changes, preserving every untouched field. Blank or null optional JSON and unsupported delivery-type clears are rejected rather than silently retained. Legacy FreeWheel products can still receive non-inventory edits; inventory changes clearly require a current Ad Unit Package.
* Your Listing page is now four steps you complete in order: Identity, What you do, Listed on Interchange marketplace, and Public distribution, where you add the paid Distribution package and point your own domain at Interchange. Once that step is live, the Distribution tabs below let you add app destinations, starting with the ChatGPT app, and each tab lists the steps it still needs. Your existing listing, domain, and ChatGPT app setup carry over, so there is nothing to redo.
## 5.464.4 — September 14, 2026 at 10:30 AM UTC
* AI Business Rules now keeps a failed policy draft and shows the save reason, recommended action, and support correlation ID.
## 5.464.3 — September 14, 2026 at 10:09 AM UTC
* In Configure connection, you can load a published contract as a new higher-version draft. The published version remains unchanged; retry if its canonical definition cannot be read.
* Creatives pinned to a format from an earlier product discovery now stay attached when rediscovery returns the same requirements under a new synthetic format ID. Interchange uses the current format automatically when the media buy runs; nothing changes on your side.
## 5.464.0 — September 14, 2026 at 8:50 AM UTC
* FreeWheel product drafts in Inventory Components now require an Ad Unit Package and keep it alongside the selected site or site group. Empty package lists and package-loading failures are shown separately so an incomplete draft cannot advance to review. Product creation for other ad servers is unchanged.
* Buyers can now add an agent to a room they own.
* Buyer administrators now see clearer agent setup, access, lifecycle, and error guidance.
## 5.463.1 — September 14, 2026 at 7:30 AM UTC
* The Inventory Selector now separates FreeWheel targeting from delivery binding. Sellers can choose where a product runs using sites, sections, site groups, series, or video groups, then independently choose the ad-unit packages that provide its delivery slots.
## 5.463.0 — September 14, 2026 at 6:14 AM UTC
* Sellers can now create a Sales Agent in the standard Agent workspace, attach it to a new inventory Source without leaving that workspace, test its `get_products` path directly without spending, and follow an end-to-end guide for validation, diagnostics, certification, and distribution. Source creation and attachment now complete as one operation, so an interrupted response can be recovered by refreshing the Agent instead of submitting the connection form again.
* When you try to change the agents in a room you do not own, the error now says you do not own the room instead of reporting that the room was not found.
## 5.462.0 — September 14, 2026 at 5:33 AM UTC
* Use `upload_creative_asset` in Buyer V2 or V3 to upload a JPEG, PNG, or MP4 without sending the file through chat. Uploading alone does not create or attach a Creative. To use the asset in a campaign, call V3 `save_creative`, bind the returned asset reference to each required canonical format slot — including a thumbnail for video — and select a destination before provider delivery can begin.
## 5.461.0 — September 14, 2026 at 4:33 AM UTC
* Snap Pixel-optimized media buys (Web Conversions and other pixel-based products) now get a clear, actionable error when the selected Snap Pixel doesn't yet have enough tracked conversion events, instead of an opaque "ad platform rejected the request" error. When Interchange can confirm this up front, it refuses the buy before creating anything on Snap, so the buyer sees the real reason (and can wait for more events to accumulate, or choose a different product/optimization goal) rather than a rolled-back campaign with no explanation.
## 5.460.0 — September 14, 2026 at 3:05 AM UTC
* Enrolled buyers can use V3 `search` and `get` to discover registered Creative Engines, then use `save_connection` to authorize an engine, select a provider account and map it to an advertiser. Existing seller connection calls and their default results remain unchanged. This preview covers registered engines only. It does not include the Creative Engines Page, V3 generation-session tools, pricing or settlement, provider qualification, or arbitrary external MCP services.
* In the existing V2 creative workflow, AudioStack voiceovers now render through Audioform at the script's natural spoken length without background music. API-key connections expose one credential-scoped provider account for advertiser mapping before generation. When an image-generation request starts with brand context but no prior creative assets, it is treated as a new draft: Interchange preserves the initial brief and creates the requested variants.
## 5.459.2 — September 14, 2026 at 2:47 AM UTC
* Fixed `sync_creatives` (REST and MCP `api_call`) failing with `account_grant_inactive` for direct/official-adapter storefronts (Meta, TikTok, Snap, and similar). These storefronts authorize buyer accounts through the adapter's own credential, not through the storefront relationship/grant model, and the buyer-facing sync path was checking the wrong one.
## 5.458.4 — September 13, 2026 at 11:19 PM UTC
* Sellers enrolled in the Product Marketing corpus can open their material Page from Teach my agent without starting a chat. Sellers outside the rollout keep the guided conversation.
## 5.458.3 — September 13, 2026 at 10:17 PM UTC
* OpenAI submission packages now download successfully while the Listing page refreshes their status.
## 5.458.2 — September 13, 2026 at 9:54 PM UTC
* Fixed two issues that could block getting a creative live with a sales agent. `list_creatives`, `list_campaign_creative_manifests`, and `sync_creatives` now find every creative attached to a campaign — some attachments were previously invisible to these calls, which blocked retrying a sync after fixing a rejected creative. Separately, a click-through URL now must be provided in the URL field; providing it as plain text is rejected with a clear error instead of being silently dropped every time the creative syncs.
## 5.458.1 — September 13, 2026 at 9:38 PM UTC
* Hands-on buyer teams using REST API, MCP, or S2S proposal-request integrations now keep each response bound to the exact campaign revision and brief that created it. A reused provider proposal ID cannot be attributed to a different request. No action is needed and existing integration contracts are unchanged. Seller indirect audience is not applicable: this changes no seller-facing surface or contract.
## 5.455.1 — September 13, 2026 at 6:06 PM UTC
* Fixed a sign-in loop that returned users to the login page immediately after successful authentication.
## 5.455.0 — September 13, 2026 at 5:43 PM UTC
* Make the Demo Environment Notice open reliably and keep the attached-source count grammatically correct after live source changes.
## 5.453.0 — September 13, 2026 at 4:28 PM UTC
* Browser authentication now requires WorkOS session tokens to target the configured application.
## 5.452.0 — September 13, 2026 at 3:23 PM UTC
* Agents now opens and stays inside the standard seller workspace, preserving workspace navigation, chats, settings, and the composer while an Agent is selected. Partner-entitled organizations also receive the same Agent collection and creation launch access as enrolled organizations.
## 5.451.0 — September 13, 2026 at 2:37 PM UTC
* Show creative upload byte and inspection progress, keep Claude informed as inspection advances, and show a completed state when a host retains the widget.
## 5.449.0 — September 13, 2026 at 1:20 PM UTC
* Choose an Interchange plugin package for the job you need to do. Buyers get separate packages for account setup, campaign management, creative management, and reporting. Agent developers get a sales-agent testing package. Agentic Media Companies get packages for listing, merchandising, distribution, campaign management, and self-serve buying.
* Storefront operators can now see the Anthropic and Claude distribution path alongside OpenAI. OpenAI reviewer access lasts 90 days after the reviewer confirms receipt, and downloading the same submission bundle more than once no longer creates a conflict.
* Installing a package does not change pricing, storefront listings, or account access. Existing Interchange plans and OAuth permissions still decide which tools and accounts each person can use.
## 5.447.1 — September 13, 2026 at 12:29 PM UTC
* When your AI assistant sends you to Interchange to upload a creative, the campaign page now accepts private MP4 uploads up to 2 GB instead of rejecting files larger than 50 MB. After inspection, the page keeps a visible upload receipt. MP4 uploads still only store and inspect the file; creating or attaching a campaign creative remains a separate step.
## 5.447.0 — September 13, 2026 at 12:08 PM UTC
* People using Interchange through chat or the UI do not need to configure anything new. Buyers and sellers may be shown an approved public agreement as reference material, but this does not accept terms, reveal private agreements, or decide which agreement governs an account.
* MCP builders can discover approved public agreement revisions through the existing `docs` search source, then read an exact revision or section and continue a long read without starting over. API developers can read the same immutable public text or download the complete document through the focused `/api/v3/documents` resources.
* Monetization: none. These read-only resources do not change packaging, billing, or feature access.
* Measurement: monitor successful, not-found, and unavailable reads for the two public-document HTTP operations and exact-revision MCP reads. HTTP and MCP acceptance checks must return the same approved revision and content hash.
## 5.445.0 — September 13, 2026 at 11:18 AM UTC
* Hosted buyer-agent operators can now give a deployed runtime a short-lived, read-only Interchange identity instead of storing a long-lived credential in the runtime. The deployment orchestrator provisions an installation binding, mints a token with a 15-minute default lifetime, and refreshes it before expiry; normal buyer credentials, human sessions, and the runtime token itself cannot call those endpoints or gain administration authority.
* This is opt-in for hosted runtimes only. Existing buyer automations and seller workflows do not change, and sellers have no new UI, ranking, inventory, or authorization effect. There is no pricing, entitlement, billable-surface, or Interchange Unit impact. The hosted-runtime handoff guide documents the orchestrator-only flow and its fail-closed binding prerequisite.
* Configured means an orchestrator has provisioned an active runtime-token binding for an installation; using means it has successfully minted or refreshed a runtime token. Track successful provisioning, mint, refresh, and runtime-token authentication events, alongside rejected mint/refresh requests and lifecycle-invalidated token attempts, before expanding hosted creation.
* Storefront connections now use AdCP 3.1 by default. Requests pinned to stable AdCP 3.2 use the compatible 3.1 contract, while prerelease 3.2 requests return a clear unsupported-version error. Clients already using an advertised Storefront version do not need to make any changes.
## 5.444.0 — September 13, 2026 at 10:36 AM UTC
* On `/mcp/v3`, documentation search now prioritizes public guidance for the active Buyer, Seller, or Organisation account and explains each result's relevance. MCP builders can pass `docsScope: "public_research"` to search across all public documentation; this changes discovery only and never grants access to private content or customer objects. Existing clients need no action, and the change does not affect how buyers see sellers.
## 5.442.7 — September 13, 2026 at 8:17 AM UTC
* `update_campaign` now always protects against two concurrent updates to the same campaign silently overwriting each other. Previously, this concurrency check only ran when the caller supplied `expectedRevision`; now the campaign's own revision is always used as the check, even when the caller doesn't. If a concurrent update wins the race, you now get a `409 CONFLICT` telling you to re-read and retry, instead of silently reverting the other request's already-applied change.
## 5.442.4 — September 13, 2026 at 6:52 AM UTC
* Murph escalation triage now probes only public, credential-free URLs, validates every redirect, and pins validated DNS destinations before connecting.
## 5.442.0 — September 13, 2026 at 5:15 AM UTC
* Printable discovery-card downloads now allow a cold browser start up to 15 seconds before timing out.
* Amazon campaign end dates now stay on the selected UTC calendar day when media buys are updated. Nothing to change on your side.
* Murph chat recents remain responsive when an older conversation needs its title repaired and its account details are unavailable.
* Show a current first-party Discovery submission package as ready for the OpenAI portal without requiring a second custom app hostname.
* New Listing + Distribution offers include unlimited self-serve advertiser invitations and management instead of a ten-advertiser allowance. Existing accepted and negotiated offers continue to use the self-serve advertiser capacity stated in their terms.
## 5.441.3 — September 13, 2026 at 4:28 AM UTC
* Sellers with a configured third-party Agent source can now view bounded physical transport attempts alongside logical Calls in Murph. Source-attributed and shared-Agent evidence remain separately labeled, and an evidence-read outage does not hide the existing Calls view.
* Buyer-side behaviour is unchanged. This adds no billable surface, packaging, unit economics, or product differentiation. It introduces no new adoption event because it is an additive view within an existing diagnostic journey; the existing authenticated request-audit record for `GET /api/v2/murph/agent-debug-calls`, together with the returned bounded transport-attempt counts, provides the usage and acceptance signal.
* Storefront Agent Page access now follows a stable application-owned account profile, preserving the existing experience independently of analytics targeting changes.
## 5.441.2 — September 13, 2026 at 3:48 AM UTC
* Verify the active platform route before atomically transferring first-party Distribution hosting, so a public listing cutover can complete without requiring the replacement route to be live first.
## 5.440.0 — September 13, 2026 at 2:32 AM UTC
* After completing buyer qualification for the pre-launch program, the success screen now makes clear that your details are still under review and that the solutions team will follow up.
## 5.439.0 — September 13, 2026 at 2:13 AM UTC
* Keep non-interactive creative upload deeplinks on the requested advertiser and campaign while the buyer workspace initializes its chat scope.
* In v3 MCP, you can now use `save_advertiser` to create, update, and archive test-only sandbox advertisers before claiming your buyer operator identity. Nothing changes unless you want to test in sandbox; live advertisers and all other readiness requirements still apply.
## 5.438.0 — September 13, 2026 at 1:36 AM UTC
* Organization administrators can register an Agent's current observed implementation as an immutable production revision without interrupting active Inventory Sources.
## 5.437.0 — September 13, 2026 at 12:55 AM UTC
* Buyer integrators now have one architecture guide that distinguishes an interactive MCP client or application from a registered buyer agent, while covering external-agent administration, optional creative pipelines, the portable asset-and-creative handoff, hosted-orchestration limits, approval boundaries, launch confirmation, and reporting ownership.
* MCP v3 now lists both Buyer and Seller tools for people who can operate in both account kinds, while each call remains authorized against the selected account.
* Sponsoring sellers can now edit a client's self-serve campaigns directly from the shared rail — no need to route changes back through the client. Every edit made this way is labeled "Edited by \[seller organization] (operator)" so both the client and the seller always know who made the change.
* **Persona pass.**
* **Seller:** can make the same campaign changes available to its client from the sponsored Campaigns Page. No separate setup is needed.
* **Sponsored self-serve buyer:** sees each sponsoring-seller edit in campaign history as an operator action, including the seller organization. The buyer can continue editing the same fields itself and needs no action.
* **Packaging and monetization.**
* No pricing, entitlement, or metering change: this extends the existing seller campaign-management workflow for an already sponsored self-serve account.
* **Availability and exposure.**
* This ships without a new flag because the request is already parameter-gated: only an active sponsorship, the seller's existing campaign-management capability, a self-serve child advertiser, and the seller-owned Storefront prove access. It does not expand to External, Delegated, advertiser-scoped, or child-bound principals. The current ruling is the expansion boundary; a future product decision may limit it to oversight-only fields. Immediately suspend the sponsorship or the seller's campaign-management eligibility to remove access.
* **Measurement.**
* **Acceptance:** a sponsored seller can edit an eligible client campaign and both the client history and seller activity list show the operator line; every ineligible principal is denied.
* **Adoption:** first successful `save_campaign` or `save_advertiser` audit record with `parameters.operator.role = operator` per sponsoring seller.
* **Usage:** successful operator-attributed campaign edits per sponsored buyer account, monitored alongside denied scope proofs and write failures.
## 5.436.0 — September 12, 2026 at 11:38 PM UTC
* Seller administrators can now complete Distribution and ChatGPT setup in the Branding & distribution Page, while technical operators can run the same guarded steps with an MCP agent. Both paths edit one shared storefront profile and clearly separate it from ChatGPT-only category, hostname, verification, reviewer access, and submission-package settings. Leave both icon overrides empty to use resolved brand identity automatically, or upload both the directory and composer PNGs; new one-sided manual uploads are rejected. Existing one-sided icon setups continue working during migration, so existing apps need no action. Printable PDF cards remain a Page-only download so their bytes never enter an agent conversation.
## 5.435.0 — September 12, 2026 at 11:16 PM UTC
* In Interchange's buyer console, the Agents page now shows a buyer agent's name, lifecycle state, credential count, and advertiser-access summary in one place, and lets an administrator rename it, or suspend, resume, and retire it directly. Suspending or retiring still requires retyping the agent's exact name and retiring is permanent — none of this changes advertiser access or billing; access is still granted separately on the Access tab.
## 5.434.0 — September 12, 2026 at 10:40 PM UTC
* Directly authenticated buyer-organization administrators can now create an external buyer agent, rename it, set its exact advertiser access, and suspend, resume, or retire it from their agent workflow. Hosted buyer-agent creation is not available here, and existing agent connections do not change. Start with the buyer-agent credentials guide. This does not change what buyers see about sellers, seller storefront presentation, or seller setup.
## 5.431.0 — September 12, 2026 at 8:10 PM UTC
* Allow V3 connections to switch accounts before Terms acceptance so they can reach the account that needs setup and inspect its next action.
* Return an authenticated Interchange upload deeplink when an MCP client cannot render the creative upload Task, preserving advertiser and campaign context.
## 5.430.0 — September 12, 2026 at 7:40 PM UTC
* The Listing page now separates the current listing status from its publish destinations. A printable card appears only after a revision is live, and a failed publish names the attempted destination while keeping the confirmed status visible.
* ChatGPT setup now leads with one next action, with detailed configuration under App details. Temporary reviewer-status failures can be retried, while a missing entitlement points to Plan & Billing.
* No action is required. Buyer-facing discovery results are unchanged.
* When you ask Murph to find advertising options, it now follows the same search while sellers respond and keeps earlier options as more arrive. It tells you whether a search finished with no matches or is still waiting, and checks that your selections belong to that search before saving them. You can correct a request rejected before the search starts and continue in the same conversation. No setup is required.
* For buyers using REST, MCP, or server-to-server integrations outside Murph, continue using your client's polling policy and the existing execution ID, campaign revision, idempotency key, and result cursors. This update adds no required request fields or integration setup; direct clients still control their own waiting and retries.
* Sales Agent validation from the Agent Page and Source Diagnostics now keeps one deterministic run in flight per action and returns its run-specific zero-model-token receipt.
## 5.428.0 — September 12, 2026 at 6:28 PM UTC
* Scrappy builder and power-buyer teams can now inspect a registered buyer agent in the Interchange API, including its identity, lifecycle, active credential facts, exact advertiser grants, and notification configuration. It gives the people building and testing an automation one place to check what Interchange recognizes without treating a credential or organization membership as permission to buy.
* Existing integrations require no change. Access remains limited to directly authenticated organization administrators and an agent reading only its own record; advertiser access remains an explicit grant. Enterprise, agency, and chat buyer workflows do not change. Sellers’ storefronts, inventory, ranking, and seller-facing experience do not change. This is included with the existing buyer API: it adds no entitlement, price, billing, or Interchange Unit use.
* The capability is alpha and structurally admin-gated, with no deployment flag. It is an atomic release: expansion is limited to eligible directly authenticated organization administrators after direct-human authorization, cross-organization isolation, zero-authority behavior, and grant-readback validation pass. Any unexpected authorization, cross-organization result, or authority without an explicit grant pauses the release and triggers rollback of this buyer-agent read surface; existing authentication and buying workflows remain available.
* We will measure first buyer-agent inspection per organization as adoption; repeat buyer-agent readback as usage; and buyer-agent read failures, grant-readback failures, and rejected non-direct-human, cross-organization, or over-broad search requests as guardrails. At 30/60/90 days, review adoption, repeat use, error/rejection rates, and whether documentation clarifies the common safe setup question; none of these signals weakens the authorization boundary.
* Add revision-fenced advertiser access editing to buyer-agent management.
* Buyer-agent administrators grant each connected agent read or read-write access to exact advertisers from the agent's Access tab; the editor keeps unsaved changes until Save or Discard and reloads on a revision conflict.
* Sellers are not affected: this changes only a buyer organization's own agent-to-advertiser grants and nothing a buyer sees about a seller.
* Monetization: no new billable surface. Advertiser access control is part of the buyer-agent management included with the buyer plan; it makes hosted and external agents safe to connect, which is the precondition for agent-driven spend on the platform.
* Creative submissions to direct provider storefronts now use the authenticated provider account through manual review instead of incorrectly requiring a managed storefront account grant.
* Fixed an Interchange-side response handoff delay that could incorrectly mark configured seller storefronts unhealthy during account-scoped product discovery. No seller action is needed; submitted-task recovery and retries retain the original account context.
## 5.427.0 — September 12, 2026 at 5:05 PM UTC
* No action is needed. Reporting polls for configured Agent sources now leave safer, more complete diagnostic evidence for support investigations, and reporting continues even when that evidence cannot be recorded.
## 5.426.0 — September 12, 2026 at 4:24 PM UTC
* Buyer access links from Scope3 now open the Interchange signup flow directly, preserve campaign attribution through sign-in, and collect the same required qualification details on every waitlist path.
## 5.425.0 — September 12, 2026 at 3:56 PM UTC
* Buyers can now configure per-campaign autonomy settings on `save_campaign` that persist across sessions. Two independent dimensions -- `inventorySelection` and `rebriefing` -- each accept one of three modes: `manual` (buyer acts), `propose` (agent drafts, buyer approves), or `automatic` (agent acts without per-action confirmation). The preference survives session restarts; omitting a dimension on update leaves it unchanged.
* Sellers are not affected: autonomy is buyer-configured campaign behavior with no seller-visible effect on storefront, ranking, or presentation.
* Plan & Billing no longer shows an empty Orders & quotes card before that functionality is available. Pending IU offers remain under Plan & pricing, and accepted plans remain in IU plan history. No action is required.
* V3 buyers can inspect a selected product's declared demographic-overlay support and age ranges while reading proposals. When capability details do not fit in one result, a signed cursor returns the next page from the same completed request or saved proposal and identifies when the result is complete.
* This is discovery and readback only. It does not activate public AdCP 3.2 execution or send demographic selections to sellers: application seller transport remains on AdCP 3.1, so `save_media_buy` rejects demographic overlays before seller dispatch. Existing seller request shapes do not change.
* There is no plan, entitlement, pricing, or usage-charge change in this slice. Existing `save_media_buy` calls without demographic age-range selections are unchanged.
* **Sellers:** No action is required. Buyers can read existing declared capability facts, but Scope3 does not send a new demographic targeting request to an existing seller connection.
## 5.424.0 — September 12, 2026 at 2:50 PM UTC
* Your campaigns can now carry structured targeting intent. Pass a `targeting` object to `save_campaign` on the V3 MCP surface (`/mcp/v3`) and the campaign stores it — geography (countries and DMAs), language, device type (CTV, mobile, desktop), daypart, and demographic dimensions. Targeting is returned in the campaign workspace so any subsequent read sees what was set.
* For teams managing multiple campaigns at scale, targeting intent travels with the campaign object in a single workspace read — no separate lookup needed. For programmatic integrations, each dimension is an array of requirement entries with a stable `requirementId`, a `strength` of `required` or `preferred`, and the values to include or exclude; update targeting by calling `save_campaign` again with the revised object. For buyers working through Murph or a chat surface, targeting set via the tool appears in the campaign workspace immediately.
* The `targeting` parameter is optional — existing calls and campaigns are unaffected. Sellers: this is buyer-side only and does not affect storefront ranking, readiness, or presentation. Delivery enforcement is tracked separately and is not yet active.
* This ships ungated: `targeting` is purely additive. Every buyer account can use it from day one; callers that omit the field see today's behavior unchanged. No billing change — targeting is campaign metadata, not a separately metered surface.
* Murph now routes unqualified questions about what teammates were chatting about to conversation search and shows safe evidence from the title, participant, or message that matched, including older message matches outside the recent transcript preview. This lets Murph answer from the result instead of trying Slack or incorrectly reporting that only recent messages were searched.
* Buyer agents can now configure campaign optimization goals through `save_campaign` on the V3 buyer surface. Supply `optimizationGoals` as a ranked array — the primary goal is at index 0, with up to 10 goals total.
* Two goal kinds are supported. An `event` goal targets conversion events delivered by configured event sources, with optional cost\_per, ROAS, or maximize\_value targets and attribution windows. A `metric` goal targets a seller-tracked delivery metric (clicks, views, completed\_views, viewed\_seconds, attention\_seconds, attention\_score, engagements, follows, saves, profile\_visits, reach), with optional cost\_per or threshold\_rate targets.
* Campaigns that do not include `optimizationGoals` are unaffected — seller execution for those campaigns is unchanged. Sellers receive the ranked goal list during campaign execution and apply it within their delivery capabilities. This is a buyer-configured field; seller-side delivery behavior is governed by each seller's own optimization support.
## 5.423.1 — September 12, 2026 at 12:43 PM UTC
* Creative uploads opened in Claude can now send selected files directly to private storage, and failed browser transfers are reported to operators without including creative content, filenames, or signed upload details.
## 5.423.0 — September 12, 2026 at 11:17 AM UTC
* Clarify neutral no-match, stalled, and genuine run failures in seller run details, with safe recovery guidance and recorded buyer identity. Buyer identity remains available only to the authorized seller of the recorded storefront; this introduces no buyer-facing lookup or inferred account mapping.
* Browser SSO sign-ins now complete reliably. Existing sign-in methods are unchanged.
* Asking an AI agent to "show me all sellers" no longer triggers both a seller search and the Connections page at the same time. The agent now routes seller and connection discovery exclusively through `open_connections_page`. No changes required for programmatic callers — `search(kind: seller)` and `search(kind: connection)` remain valid on the V3 wire contract.
* The `save_campaign` tool now correctly labels flight dates as datetimes and rejects a start in the past, preventing the midnight-UTC same-day start bug and grounding date arithmetic.
* `get_products` (and `discover-products`) no longer stalls a cold-start caller with an opaque advertiser-context error. If your account has exactly one active advertiser we now use it silently, so a request with no `ext.interchange.advertiser_id`, `X-Advertiser-ID`, or advertiser-scoped credential just works. If your account has several (or none), the error now names each candidate as `adv_` so your agent can pick the right one on the next call. Explicit sources still win — no change is required if you already pass an advertiser id.
* If your seller account is outside a request's market or channel, you will no longer receive a brief for that request. No action is needed from sellers, and buyers have no new workflow or action to take.
* The Merchandising Agent interview now separates facts awaiting confirmation and gaps that block a practice brief from optional delivery setup needed only before live activation. Practice proposals also distinguish source-backed facts, assumptions, and manual confirmation instead of inventing commercial or delivery details.
* Product Marketing now summarizes confirmed selling points, pending reviews, processing material, and recorded gaps before you inspect each source.
## 5.422.0 — September 12, 2026 at 10:17 AM UTC
* Buyer delivery reports now show flight-to-date totals when sales agents send daily delivery using an end-exclusive reporting window. Daily buy and package counters are retained in the delivery ledger and accumulated across the flight; no action is required from buyers or sellers.
* Through the V3 MCP, seller agents can now find and inspect seller-owned account relationships with external buyers, admit or decline seller-controlled pending requests through `save_seller`, and open the existing relationship-scoped Media Buys and Delivery page. Decisions use the request's current version, so stale writes cannot overwrite a newer decision. Existing relationships and buyer-side admission behavior are unchanged; no action is required unless you want your seller agent to use this workflow.
* Campaign reads now include a `readiness` field with a `status` (`ready` or `not_ready`) and specific `reasons` (`no_budget`, `no_staged_buys`, `flight_not_started`, `flight_ended`, `advertiser_missing`) so agents can give buyers actionable next steps instead of showing a misleading `active` phase when the campaign cannot actually deliver.
* Campaign delete no longer holds the budget lock while issuing upstream cancel requests to the seller ad server.
* The lock is now committed before each seller cancel call and re-acquired for the final check and archive write. This removes a latency spike that could block concurrent campaign edits for the full duration of a seller-cancel round trip.
* Ask Murph keeps your latest sent message and its response visible while the response or an in-chat widget loads more content. Scrolling up to review earlier messages still pauses automatic following until you return to the bottom.
* Snap media buys targeting Australia exclusively now automatically include only 18+ audiences to satisfy Snap's Australian minimum-age regulation. If you explicitly request 13-17 targeting on an AU-only buy, the buy is rejected with a clear error before it reaches Snap.
* Snap media buys no longer fail creation with a generic "The ad platform rejected the request as invalid" error caused by Snap rejecting our targeting-expansion shape as internally contradictory. The adapter now sends the standard "no expansion" signal instead of the regulated-sector-only shape it was previously using by default.
## 5.419.0 — September 12, 2026 at 1:08 AM UTC
* Murph now makes one tool-capable recovery attempt with an independent model when its primary model cannot complete a response. Account lookups and actions keep the same authorization and confirmation checks, with nothing to change on your side.
* Creative observation no longer reports the analytics beacon Cloudflare adds to pages on the Interchange render origin as a domain the creative called or as a tracking pixel. Only that injected script and its beacon post, issued from the render origin's own frame, are left out. Requests the creative makes from its own page, including to the same hosts, are still recorded; a creative that embeds Cloudflare Web Analytics inside the render frame itself is the one case the observer cannot tell apart.
* Seller Agent navigation controls now have accessible labels for assistive technology.
* Show preview on the Approvals page in the Interchange web app now loads the creative's live preview. The web app's widget host refused every frame, including the render origin, so the preview panel fell back to "Nothing to preview for this format". The host now allows the frames a widget declares, and nothing else.
## 5.418.0 — September 11, 2026 at 11:22 PM UTC
* Organization administrators can now grant a buyer agent access to specific advertisers as read-only or read-write through the buyer-agent principals REST API. Changes take effect on the agent’s next advertiser operation and notification-delivery attempt.
* Buyer agents without configured grants cannot access advertisers. Registering an agent, issuing credentials, or organization membership does not grant advertiser access. This buyer-side control does not change sellers’ storefronts, inventory, ranking, or buyer-visible seller information.
* Campaign creative creation and updates now map uploaded files to declarative repeatable format slots, including carousel cardinality validation and correction controls.
* Proposal PDF downloads render correctly in common PDF viewers.
## 5.417.0 — September 11, 2026 at 9:59 PM UTC
* On the shared `/mcp/v3` connector, select a buyer-owned advertiser to upload a local JPEG, PNG, or, for enrolled buyers, MP4 to private storage. Each upload is bound to the buyer, authenticated principal, and advertiser; it neither selects a destination nor confirms provider delivery. Finalized JPEG/PNG sources can be adopted through `save_creative`. MP4 remains upload-only on V3 pending explicit human approval of its adoption contract. The V2 provider-bound Legacy adoption path remains a separate handoff and is never a fallback.
* Activity's Calls API now returns an `mcpHost` hint for newly recorded MCP calls when it recognizes a host, such as ChatGPT or Claude; accounts with the Calls view enabled see the same label there. The caller supplies this diagnostic hint, so it is not identity or authorization evidence; it is `null` for older calls, non-MCP calls, and unrecognized hosts. Nothing to change on your side: use the Calls API, or Activity's Calls view where it is available, when you need to inspect the hint.
* Proposal composition now keeps a dedicated verification window after corrective model passes, preventing valid corrected proposals from falling back because generation consumed the verifier's entire time budget.
## 5.416.2 — September 11, 2026 at 8:25 PM UTC
* Keep seller setup tasks and collapsed widget expand controls available before a chat begins.
## 5.416.1 — September 11, 2026 at 7:46 PM UTC
* Branding & distribution now opens Plan & Billing reliably and lets every eligible Public storefront request and revoke its own ChatGPT reviewer sandbox. No action is needed.
## 5.416.0 — September 11, 2026 at 6:29 PM UTC
* Preview structurally valid modular inventory rows before catalog proof is complete, while keeping unresolved rows unavailable to buyers until their publisher, property, and format declarations are verified. Seller agents can also update an existing modular source module's non-secret configuration through the typed inventory-source save flow.
* The buyer and storefront API specs grew: shapes reused across the document are written out at each usage site, and Zod 4.5 reuses more aggressively than before, so the buyer spec restates its 78 distinct regex patterns thousands of times. The two largest offenders — the AdCP product's `placements` and `format_options` — are now published once each, as `AdcpProductPlacements` and `AdcpProductFormatOptions`, and referenced from the product schemas rather than repeated inside them. That recovers about 73,000 lines. No existing schema component was removed or renamed, and nothing about what the endpoints accept changed.
* Three `GET /activity/calls` query parameters — `includeDiagnostics`, `includeProtocol`, and `includeInternal` — now publish as `boolean` rather than as a string enum of `"true"`/`"false"`, which is what they always were on the wire. Regenerate your client to pick up the corrected type.
* Integer lower bounds now render as `exclusiveMinimum` where they previously rendered as an inclusive `minimum` one value higher. The accepted values are identical; only the spelling in the spec differs. Request validation also got substantially faster, most noticeably when rejecting a malformed request.
* Campaign resume now skips media buys that were paused directly by the buyer.
* Previously, resuming a paused campaign would reactivate all paused media buys, including ones the buyer had explicitly paused. Resuming a campaign now only reactivates buys that were paused as part of the campaign pause. A buy the buyer paused individually stays paused after a campaign resume.
* A new `pausedBy` field on the media buy surface shows whether a buy was paused by the buyer directly (`buyer`) or by a campaign pause action (`campaign`).
* When a seller sends an updated offer across negotiation rounds, each version is now stored separately and linked to the one it replaced. You can retrieve any round's proposal and trace how the seller's offer evolved -- earlier versions are no longer silently overwritten. Nothing to change on your side; proposal history is tracked automatically.
* Snap media buys submitted with `start_time: "asap"` (the default meaning "start immediately") now correctly submit an ISO timestamp to Snap instead of the literal string, so they no longer fail with a generic error on creation or update.
* Snap media buys using automatic bidding — the default for nearly every Snap product — now create successfully instead of failing with a bid-price error. Nothing to change on your side.
* Invitations now keep the intended account email design when resent, use a saved Dutch language preference when available, and render transactional email chrome from centrally configured branding. An invitation opened with a different authenticated email now explains the mismatch and alerts compliance while keeping access locked to the invited address.
* Embedded sales agent 1.43.2: resolve FreeWheel creative nodes from Placement readback.
* Restored the Merchandising Agent interview as the eligible seller's bare-chat value layer alongside the current seller setup navigation. Sellers first encounter it on initial login and can return whenever the chat canvas is empty to teach the agent how their business sells or test it with a real buyer brief; sellers without product-composition capability retain the neutral home experience.
* `get_delivery({ report: "campaign_delivery" })` on `/mcp/v3` now accepts `range: { lifetime: true }` for the campaign's full delivery history and echoes the resolved window as `period`; every response carries `totals` over all matched rows (money is unavailable when rows span currencies); and two new dimensions, `seller` and `sales_agent`, group delivery by the storefront and AdCP sales agent a buy ran with.
## 5.415.0 — September 11, 2026 at 12:37 PM UTC
* Buyers and sellers can now select **All** in Murph’s recent chats list to search and reopen past conversations for the account, advertiser, or storefront they are viewing. Earlier conversations are included; results never leave the selected scope, and nothing needs to change on your side.
* For enterprise brand buyers, this keeps previous work visible within the advertiser their media organization is reviewing. Agency buyers can return to a prior account or advertiser conversation without hunting through recent chats. Hands-on buyers can quickly pick up an earlier task, and newer Murph users will simply see **All** as another way to find a chat they already had. For sellers, this changes only their own storefront conversation history; it does not change what buyers see about a seller or storefront.
* Monetization: none — this ships with existing Murph access and adds no plan, entitlement, packaging, or billable-surface change.
* Clarified that campaign-level `frequencyCaps` is enforced through Scope3's own cross-publisher exposure tracking and cannot be honored at request time by closed-platform integrations (TikTok, Meta, Snap, Pinterest, Reddit, Spotify, Amazon, Google, LinkedIn). To set a native per-publisher frequency cap on one of those platforms, use `targetingOverlay.frequency_cap` on the product instead.
* Selected sellers now activate every eligible advertiser through the canonical account connection, including storefronts without a legacy sales-agent record. New advertisers are picked up while the seller remains selected, blocked sellers create no new advertiser accounts, and failed activations show a support code for diagnosis. Nothing changes on your side.
* The Public Distribution upgrade action in Listing now opens Plan & Billing instead of showing an error.
* The v3 buyer skill now explicitly instructs agents to poll a running proposal execution by calling `request_proposals` again with the same `campaignId`, `expectedCampaignRevision`, and `idempotencyKey`, matching the tool's own response text. The previous wording ("reuse the idempotency key to poll") was vague about which tool to call.
* **Murph buyers.** In-progress seller results now continue on the same product query instead of restarting. When you select a returned product, Murph carries that selection into the campaign without rerunning discovery. There is nothing to change on your side.
* **Direct V2 API and MCP integrations.** Reuse the original execution ID when checking progress. To attach a selection, set `productQueryId` to that execution ID and include the returned storefront-qualified product or proposal IDs, as applicable. Legacy executions without saved accepted proposals still require a fresh product query and buyer re-selection.
* **Direct V3 integrations.** Retry in-progress proposals within a bounded polling policy using the same campaign ID, expected campaign revision, and idempotency key. No endpoint or schema change is required.
* **Murph sellers.** An inventory-source diagnostic that records its test run and source-health result now asks for confirmation before it starts. Read-only inventory-source checks remain confirmation-free, so only diagnostics that save a result add this approval step.
* You can now guarantee that specific reusable Library slides appear in a composed proposal. When you call `save_rfp` with `create` or `append_turn`, pass `requiredLibraryUnitIds` with the exact identity of each unit (`materialId`, `unitId`, `renditionRevision`). Only slides, pages, or sheets you have marked reusable, that carry no pricing, and that are currently available in your Library qualify; anything else is rejected with a validation error before composition starts. The buyer's proposal then always carries those Library blocks unchanged, including when the composer falls back to the deterministic response, and the turn receipt records them as seller-required rather than model-selected. Leave the field out and proposals compose exactly as they do today. Details are on the Seller RFP lifecycle docs page (`/v2/setup/v3/rfp-lifecycle`).
* Launching a campaign from your agent now shows you exactly what would go live before anything does.
* **What changes for you**
* **The launch preview lists the media buys.** Asking to activate a campaign without confirming returns a preview that names each staged media buy that would go live, its budget and seller, and the combined budget across them, alongside the campaign budget and flight. Nothing is launched by that call.
* **The confirmation step is explicit.** The preview tells your agent the exact follow-up call — the same request with `confirmLaunch: true` and the previewed revision — so a launch always takes two intentional steps: see it, then confirm it.
* **Budgets in different currencies are never added together.** If the staged buys settle in more than one currency, the preview totals each currency separately instead of inventing a single number.
* **Do you need to do anything?**
* No. The confirmation field already existed; this release makes the preview complete. Agents that already confirm with `confirmLaunch: true` keep working unchanged.
* Sellers: not applicable.
## 5.414.0 — September 10, 2026 at 11:54 PM UTC
* In the Buyers tab, select an external buyer to see the media buys delivering on your inventory — impressions, seller-reported net spend, progress toward the seller-visible booked budget when available, and the latest reported delivery day. Nothing to configure. Buyers awaiting admission and buys without a delivery report remain clearly unavailable rather than showing zero.
* Clarify available IU offers, current plans, and the no-plan state.
* Keep the Available balance explanation visible inside its overview card.
* Open balance details directly at Standing & funding.
* Give Interchange and media invoice activity distinct section headings.
* Label unbound IU accounts as having no paid plan instead of pay-as-you-go.
* Explain the action or payment issue causing an account to need attention.
* Creative sync now succeeds for media buys placed through a connected ad-server account (Google, Meta, Snap, TikTok, Reddit, Spotify, Amazon, or AudioStack). Previously, creatives could silently fail to sync to these media buys after they were created. Nothing to change on your side.
* The Sellers page now tells you to add an advertiser's markets and channels instead of saying sellers don't match, and offers the setup step.
* Managed storefront creative syncs now reuse approved account links when creatives are saved again or explicitly resynced. Incomplete links are blocked before sync starts and return an actionable account or seller-mapping error.
* In a storefront's Advertisers tab, Sellers now opens the advertiser's sellers page instead of doing nothing.
* A media buy whose underlying source connection was removed or never fully configured no longer stays stuck indefinitely with a status that never updates — it now resolves to canceled after a bounded number of retries. Nothing to change on your side.
* Corrected composed-product prices so size-specific rate-card facts apply only to products that actually include those selected creative dimensions.
* Preserve organization-local browser history while moving between billing tabs.
* Use Available balance consistently across Plan & Billing.
* Show billed Intelligence Units on Overview instead of provider operating cost.
## 5.413.8 — September 10, 2026 at 11:09 PM UTC
* When Murph is waiting for a storefront during product discovery, it now continues the same search instead of treating a provisional empty response as final or starting over. Direct API responses are unchanged, and there is nothing to change on your side.
## 5.413.0 — September 10, 2026 at 11:57 AM UTC
* Enterprise buyer teams can govern their buyer agent's organization-owned machine notification subscriptions through the buyer API. Teams building their own buyer-agent integration can create or replace, pause, reactivate, remove, and non-business-test an endpoint. Changes are fenced, endpoint credentials remain write-only, and only deliverable event types are offered. This does not change any seller-owned account or destination.
* See a clearly labeled fictional Storefront before creating an account. This display-only walkthrough uses preloaded sample data, runs no analytics, and saves nothing. Create an account to add your inventory or connect a production system.
* Campaign creative creation and updates now map uploaded files to declarative repeatable format slots, including carousel cardinality validation and correction controls.
* Updating a campaign's country or region targeting (`constraints.geo_countries` / `constraints.geo_regions`) now correctly applies to media buys that are already live, instead of only affecting future product discovery. `update_campaign` reports the outcome per media buy (applied, unchanged, or rejected) so a targeting change a seller can't accept on an already-live buy is never silently dropped. Sellers see this the same way they already see any other live targeting change: through the existing per-package targeting-overlay update.
* Separately: any update to a campaign — not only ones that change targeting — now returns `409 CONFLICT` if it arrives while another update to that same campaign is still being applied, instead of racing it. This closes a gap where an unrelated concurrent change (for example, a rename) could silently undo a targeting change that had already taken effect. If you see this error, retry the request.
* Proposal PDF downloads render correctly in common PDF viewers.
## 5.411.0 — September 10, 2026 at 8:18 AM UTC
* Campaign creative format pickers now show only formats declared by that campaign and preserve the selected canonical option identity.
## 5.410.2 — September 10, 2026 at 7:31 AM UTC
* Listing + Distribution now unlocks automatically once an account has the package's distribution entitlements. No action is needed.
* When a creative submitted to your storefront cannot be filed for review, the failure is now recorded with its reason instead of disappearing. The buyer's sync result names the creative and carries a typed error, and the storefront operator sees the exact cause in logs and error tracking, so a creative that never reached your Approvals queue can be traced rather than guessed at.
## 5.410.1 — September 10, 2026 at 6:23 AM UTC
* `update_media_buy` errors are more actionable, with no change to which requests are accepted or rejected. An unsupported update field now tells you to remove that field and resubmit the rest of the request separately, instead of a generic "Upstream request failed". A package creative that fails format-compatibility validation now names the exact creative and the reason it was rejected, instead of a generic "does not match its product's canonical format options" sentence.
## 5.410.0 — September 10, 2026 at 5:53 AM UTC
* Fixed creative assignment to a Meta-backed media buy, which failed end-to-end after Meta rejected `sync_creatives` for requesting webhook completion tracking it doesn't support, and then rejected the `update_media_buy` fallback too. Creative sync to a synchronous adapter-backed storefront (Meta, and any other first-party ad-platform connection) no longer requests webhook tracking meant for asynchronous external sales agents, so `sync_creatives` now completes directly.
## 5.409.0 — September 10, 2026 at 5:02 AM UTC
* Pass a finalized private JPEG or PNG upload as `sourceAssetRef` to `save_creative` to create a durable Creative; existing calls are unchanged. Interchange confirms that the upload belongs to the connected account and the Creative belongs to the advertiser, then saves a managed copy. For source provenance, the Creative stores only a one-way fingerprint, never the private source reference or a signed storage URL. The upload Task activity keeps the private reference available until the handoff completes. Retrying the same source and name returns the same Creative, and you can optionally associate it with a campaign. Raw REST creative creation and package or media-buy attachment remain separate.
* Storefront discovery now retains submitted third-party seller tasks and continues them through account-scoped catch-up polling when an interactive deadline ends.
* Agent-composed proposals now allow the model up to 90 seconds before falling back to the plain summary, so a slow answer no longer discards the endorsed shape or Library material you asked it to follow.
## 5.408.2 — September 10, 2026 at 4:02 AM UTC
* When a proposal required to follow an endorsed response hits a transient verifier model error and time remains, it now gets one more verification pass instead of falling back to the plain summary. Proposal receipts identify verifier outcomes and retain the cost of verification passes even when the proposal falls back.
## 5.408.1 — September 10, 2026 at 3:24 AM UTC
* A product you price on your ad-server source, or that carries a platform-derived floor, can now be pitched by the proposal composer, and its product page shows the same completeness verdict the composer uses. A bare non-fixed rate without a fixed or floor price is not quotable.
* Media buys created directly against a Meta, TikTok, Snap, or other platform-adapter storefront now record their routing correctly, so creative sync and other post-buy operations can find them instead of silently failing.
## 5.408.0 — September 10, 2026 at 1:52 AM UTC
* Products from Google Ad Manager now carry a channel inferred from an unambiguous canonical creative format when the source leaves channels blank. You can also set channels yourself when saving a product, and the proposal composer can pitch those products once a complete price is present.
* Correction: uploaded TikTok creatives are still affected. Release 5.404.0 reported this as fixed, and that was incorrect.
* Release 5.404.0 said uploaded TikTok image and video creatives now become ads, and gave TikTok's media processing time as the reason they previously did not. Both statements were wrong. Uploaded creatives still reach your TikTok creative library and are still not attached to an ad.
* Nothing you uploaded has been lost. Those creatives are in your TikTok creative library and remain usable directly in TikTok Ads Manager in the meantime, and no action is needed from you. We are still working on the underlying cause and will report again only once it is confirmed working end to end.
* Buyer administrators with publication access can now open Branding & distribution from their designated account. No action is needed.
* Inventory Selector can now open the secure ad-server connection task when an ad server needs to be connected.
* Google and SSO signup now moves directly to company confirmation while company research runs in the background. Academic mailbox subdomains are normalized to their institution domain and treated as affiliation context, never evidence of a sell-side media business.
* When you endorse a response and a later brief fits it, the composed proposal now follows that endorsed shape instead of falling back to the plain summary.
## 5.407.1 — September 9, 2026 at 9:22 PM UTC
* When a media buy uses a seller creative library, retries now reference existing creative IDs instead of resending them as new creatives. Inline delivery for sellers without a creative library is unchanged, and buyers do not need to modify their creatives.
## 5.407.0 — September 9, 2026 at 8:19 PM UTC
* Murph can now accept WAV files that Slack or a browser labels with the common vendor MIME type, and a direct request in a Slack thread can use a file shared earlier in that thread.
* Campaigns, media buys, and advertisers on the v3 buyer surface now do what they say, and you can undo more without leaving your agent.
* **What changes for you**
* **You can bring an archived campaign back.** Restoring returns it as a draft, since its media buys were retired when it was archived. Stage inventory again before launching.
* **You can fix or drop a draft media buy.** Change its budget, bid, or pricing option, remove a line item, or archive an unwanted draft without touching the rest of the campaign. When the same product is staged twice, each line item now carries its own reference so you can point at exactly one. Anything the draft cannot absorb is refused with the field named, never silently ignored.
* **Archiving an advertiser no longer hides its history.** Its campaigns, media buys, and creatives stay readable (read-only until the advertiser is restored), and its name is free for a new advertiser. Restoring into a name that is now taken is refused with a clear conflict message.
* **Archive several advertisers in one go.** Ask once; your agent confirms the list once and reports each outcome. A long list that runs out of time tells you exactly which advertisers were not attempted, so you can retry just those.
* **Offers do not silently expire while you decide.** A product query left for over an hour is reopened when you stage from it, and an expired proposal says whether the seller's validity or the 90-day window lapsed.
* **Do you need to do anything?**
* No. Everything in this release is additive or corrective; existing calls keep working.
* Sellers: not applicable. Nothing a seller sees about a buyer, a proposal, or a delivered buy changes.
* `add_discovery_products` now returns a distinguishable `discovery_publication_orphaned` error when the discovery snapshot never finalized, telling you to rerun `get_products` on the **same** discovery so the snapshot republishes — the session's selected products and campaign attachment stay intact. Previously these attempts failed with `discovery_pricing_superseded` and the misleading advice to "retry against the current snapshot" — but no newer snapshot ever existed, so every retry was guaranteed to fail. The real transient race (a fresh publication is landing while your request runs) still surfaces as `discovery_pricing_superseded` and remains safe to retry. If your integration branches on `error.details.reason`, add a handler for the new `discovery_publication_orphaned` value.
* Invitation links now join you to the intended account when an organization has multiple accounts with the same name. No action is needed.
* Fixed a bug where campaign postal-code targeting for countries without a dedicated postal system (e.g. the Netherlands) could make `list_campaigns` fail to load for affected campaigns.
* For sellers enrolled in the limited Sales Library rollout, Murph chat now reliably saves material when you explicitly approve the supplied details, and proposal previews reliably use the response example you select. Buyers may see the proposal shaped by that selected example. No action is required.
* Agent-composed proposals can now follow your endorsed responses. Shaped by and Library blocks appear when a fitting endorsed pair or reusable Library material exists.
* If Source Diagnostics shows stale or conflicting health evidence for a sales-agent inventory source, you can refresh it with Murph or an MCP client. A fresh no-spend `get_products` check can supersede an older creative-sync failure, so your storefront's readiness reflects the source's latest observed catalog health. It does not change source configuration or create a media buy; review the timestamped receipt in Source Diagnostics. No refresh is needed when diagnostics does not request verification.
* This is an ungated (`gate: none`) atomic, read-only operational improvement for sellers with an already configured external sales-agent source: there is no staged audience expansion, billable surface, packaging change, or new entitlement. There is no runtime kill switch because it neither mutates source configuration nor spends; withdraw the tool in a follow-up release if it produces misleading evidence. For the first 30 days, review refresh invocation volume, successful durable receipts, and readiness-status changes. Success is replacing stale evidence with a current receipt without creating a media buy; investigate repeated failed or no-change refreshes in Source Diagnostics.
* Buyer reporting no longer fails an entire account's metrics when a seller reported delivery spend in a currency it is not actually paid in. Those buys previously made `/api/v2/buyer/reporting/metrics` return `SPEND_DENOMINATION_UNRESOLVED` (and the timeseries view mislabelled their rows). The spend is now denominated by the currency the source settles in, when that is evidenced by money that actually moved — an FX lock or a booked spread-ledger leg: unchanged when it already matches the advertiser's reporting currency, converted from the settlement currency when it does not. The `deliveryFxConversion.source` field gains a `settlement` value identifying spend denominated this way. Buys whose settlement currency cannot be established, and buys whose only settlement evidence is the product budget currency (a configuration value, not proof of what historical periods were paid in), still fail closed rather than guess.
## 5.406.0 — September 9, 2026 at 2:53 PM UTC
* Authenticated customer members can now search their own and shared Murph conversations by title, participant, or transcript text. V3 clients can also search and open a bounded conversation transcript; private teammate rooms and other accounts remain excluded, and service credentials without a member identity cannot read chat history.
* When a media buy or creative sync fails for a reason the buyer can fix, the error now carries a structured classification your agent or client can route on directly — no more regex on the human message.
* If your code doesn't consume the new field, nothing changes. Existing error messages still render the same way, and the outer `code` on the error is unchanged.
* On a buy-flow failure the buyer can act on (creative dimensions don't fit the packages, missing destination URL, budget below the source's minimum, product no longer available, permission not granted yet, account not linked, etc.), the error now includes a `buyer_reason` sub-object with a stable `code` and a message safe to show to a buyer. It travels with a `recovery` classifier (`correctable`, `transient`, or `terminal`) so you can tell "fix and resubmit" apart from "wait and retry."
* The same classification appears on each per-source entry when a fan-out mixed causes (one source rejected the creative, another was unavailable), and on each failed creative row on `sync_creatives` so a batch failure is per-creative traceable.
* The `code` follows the AdCP 3.2 error vocabulary; unknown codes are preserved end-to-end, so a newer source or a seller-defined extension flows through your client without upgrade.
* Read the current `buyer_reason.code` for programmatic routing and `buyer_reason.message` for the string you show a human.
* The Sellers page keeps loading when a connection is waiting for activation prerequisites. This does not change storefront readiness or how sellers are presented.
* ChatGPT app registrations that omit the deprecated upload fields now use the storefront logo from `brand.json` to generate directory and composer icons automatically, without requiring a separate rollout enrollment. Existing V2 upload callers keep working, and stored manual icons remain safe while their accounts migrate.
* A response you attached to a brief now shows as Paired on the Library page.
* Agent-composed proposals now complete the full model pass instead of falling back to the short summary, so Shaped by and Library blocks appear when an endorsed pair or reusable material fits.
* For sellers enrolled in the limited rollout, this restores reliable Murph chat support for saving shared material and using a selected response example while preparing a proposal. Buyers may see proposals shaped by the selected response example. No action is required.
* `get_media_buy` and `get_campaign` now include TikTok's provider-confirmed targeting (country, age, gender, and audience) alongside what you requested, so you can verify what a TikTok media buy is actually targeting without checking TikTok Ads Manager directly. Previously this readback was never returned for TikTok buys.
* Seller Page search tools now keep their complete guidance on the wire instead of cutting off the final identity instructions.
* Published agreements now have permanent, hash-verified revision URLs. Current links continue to show the maintained version, while pinned links—including the Privacy Policy and Demo Environment Notice—remain tied to the exact published text for reliable citation and historical review. A corrected v2→v3 summary restores its originally approved “MSA equivalent” wording at a new revision URL while preserving the earlier published summary bytes.
* Account admins can now review people in a connected Slack support channel from Communications. Choose an account role to send an invitation, keep someone as a channel guest, identify an external company, or disconnect the channel. Joining a channel never grants Interchange account access; disconnecting it leaves existing account memberships unchanged.
## 5.405.0 — September 9, 2026 at 11:01 AM UTC
* The advertiser picker now opens on your only advertiser, or the one you used last, so you land in the right workspace without an extra click.
* Invited team members now land in their organization after verifying a new Interchange account instead of reopening an invitation after signup already granted access. Other post-signup destinations are unchanged, and there is nothing to change on your side.
* Invited team members can finish creating their Interchange account without a signup validation error. When the invitation already belongs to their resolved organization, signup no longer offers to create a duplicate organization. Nothing needs to change on your side.
* Fixed two related Meta storefront issues: resuming a paused campaign now activates its ad set and ad along with the campaign (instead of leaving delivery silently paused underneath a reportedly-active campaign), and syncing a creative to a package no longer requires the package to already be paused first.
* Prevent capability checks from reusing the tracking ID of a following seller mutation. Media-buy updates and account syncs with webhook callbacks now reach the seller instead of failing locally with a webhook-registration conflict.
* Nothing changes in the Interchange chat experience today. Teams using a buyer or seller MCP client can now continue a Murph conversation and read its display-safe history, while existing `ask_murph` integrations keep working. There is nothing to configure and no pricing or packaging change.
* For enterprise brands, this makes a Murph conversation easier to retain in their governed workflow. For large agencies, it gives teams a consistent way to resume the same conversation across their client tools. Builders can use the new `chat` and `get_conversation` operations, including standard progress and app-display support. For people using Interchange chat directly, the behavior stays the same and requires no action. Sellers receive the same client support; it does not change how buyers see their storefront or inventory.
* This does not yet add the Interchange agent picker, calls to installed buyer agents, or agent-initiated messages.
* Rollout is ungated because this is additive on Murph's existing authenticated MCP endpoint and keeps its existing authentication, terms, and availability checks. Monitor successful and failed `chat` and `get_conversation` calls plus cursor/readback errors during the first seven days; only the later installed buyer-agent and picker slices expand the audience. Set `MURPH_ENABLED=false` to use the existing Murph kill switch if the profile needs to be withdrawn.
## 5.404.0 — September 9, 2026 at 8:40 AM UTC
* When you use an endorsed response example in a proposal, its "Shaped by" detail now stays visible. Murph also saves a complete Library dictation instead of replying without making the requested update. Nothing to change on your side.
* TikTok image and video creatives now become ads instead of stalling after upload.
* An uploaded creative reached TikTok's creative library but was then reported as failed, so no ad was created and the media buy stayed in pending creatives. TikTok processes uploaded media asynchronously, and Interchange was reading back the result before that processing finished. It now waits for TikTok to confirm the media is usable before deciding whether a creative is live, and a creative that is briefly unavailable immediately after upload is no longer treated as missing.
* Recent chats in the rail are now named for what you asked, empty chats no longer linger, and the list shows your five most recent with an All view for the rest.
## 5.403.0 — September 9, 2026 at 5:48 AM UTC
* Snap media buys using a creative that wasn't tied to a specific product at upload time — the common case for a general-purpose creative-library upload — now sync correctly instead of being rejected outright. We resolve the correct format automatically from the assigned product when it's unambiguous. No action needed on your side.
* While a Scope3 administrator used view-as to view your account, they could have seen analytics from another account. This was limited to Scope3 administrator view-as sessions; other sellers could not access your analytics. Analytics now always belong to the account being viewed. There’s nothing for you to change—your own sign-in was never affected.
* The configured first-party distribution account can now open Branding & distribution from its account navigation even when that account is classified as a buyer. Other buyer accounts remain unable to open seller-only surfaces.
* Asking Murph about your Library now goes straight to your Library materials instead of first trying the advertiser creative library.
* Sellers who submit their storefront's buyer agent to a public app directory now get a no-spend test advertiser inside that storefront and one read-only reviewer credential. Existing storefront configuration, customer advertisers, campaigns, billing, and delivery are unchanged; no action required.
## 5.402.0 — September 9, 2026 at 1:06 AM UTC
* Creative observation is now on for every seller, with no enrolment step. When a buyer submits a creative for your review, Interchange runs it once, headlessly, on its render origin and records what it actually did, and the Approvals row reads from that run instead of guessing from the source. Where thumbnail storage is configured, a picture of the frame appears beside it. Observation is evidence from one run at one time, never a safety verdict, and it never gates your decision.
* The example pack now includes Library files and two brief/response pairs so a new seller can rehearse the full proposal loop before uploading their own material.
* Opening a link for one of your other accounts now switches to that account before rendering, instead of showing the wrong navigation or an error.
* Since 2026-09-07 the Library, the demand inbox scoreboard, and proposal narratives are available to every seller, and the Playbook rules editor points to the Library, rate cards, and AI Business Rules. This release updates the documentation to match what you see: use **Add material** at the top of Materials to upload a file, and use **Shaped by** on your seller pass or preview to see the endorsed pair the response followed and why. Docs: /v2/storefront/library.
* Keep Murph-specific response rewrites out of internal TARS answers while preserving shared security and action-confirmation guardrails. Murph no longer replaces a terminology warning with unrelated inventory guidance.
## 5.401.0 — September 8, 2026 at 7:33 PM UTC
* When an admin sends an account invitation, they now receive a copy of the email so they can confirm what was sent and help resolve delivery issues.
* The Library and Demand pages now load in the Interchange web app; they previously failed to load outside chat hosts.
* For API integrators: no action is needed unless your integration called the Seller Dashboard demo evaluation through the web app's internal endpoint with an API key. That path now requires a signed-in browser session. Use the Seller Dashboard read tools on the API instead; nothing else about your API access changes.
* Murph can now file an ask after you switch into another account you manage. Staff support and impersonated sessions remain excluded from customer-authored requests. If an MCP session authenticated with a user or personal API key was already open during the release, reconnect it once before filing; this includes switched sessions.
* Murph no longer fails at the start of a Gemini-backed conversation when the Seller V3 tool catalog contains recursive JSON values.
* Delivery reports whose response-wide currency contradicts the currency their packages declare no longer strand spend as unbooked. The reconcile sweep and the admin replay path now denominate a re-driven delivery the way the report processor already does — the currency every package agrees on takes precedence over the deprecated response-wide one — so spend that a mismatched currency kept out of the ledger books automatically on the next sweep.
* A report that sends an empty currency now falls through to the next level that declared one, rather than being read as a declaration of nothing; previously a report whose packages all sent `""` booked no spend at all even when its response-wide currency was valid.
* Reject placeholder `unknown` brand and account identities before consulting cached product-discovery results, so stale cache entries cannot mask invalid seller-bound identity.
* When you select an Agent, its workspace now stays in your Murph conversation with your chat history and message composer available. Nothing to change on your side.
* On Seller settings, Resume intake now clears a seller-owned hold even if the readiness panel still needs attention. No extra action is needed to clear the hold; readiness, runtime, and administrator safeguards still control whether new transactions can proceed. This does not bypass buyer transaction eligibility safeguards.
* On Agent collection pages and Murph, active Inventory Sources with an explicit authorized binding now appear consistently. These visibility corrections do not change an Agent's details, source ownership, or buyer transaction behavior.
* Media buy and campaign product reads now show the selling agent again for buys whose seller moved to typed storefront routing, and their product and publisher names resolve as before. No buyer action is required.
* When sellers open Advertisers, they can select an account they operate from a shared advertiser picker and use its buyer work rail. Protected System accounts remain read-only. External counterparties continue to appear in Buyers, while Merchandise, Operate, and Organization settings remain in Inventory (or the Settings footer); no action is required.
* `search(kind: "media_buy")` on `/mcp/v3` now accepts `filter.advertiserId` without a `campaignId`, returning every current media buy for the advertiser across its campaigns in one paged read (`limit` up to 200, `nextCursor` for the next page). Each row carries its `campaignId`. The campaign-scoped read is unchanged.
* In-chat widgets now render as one consistent card in every state. Fixes extra padding around widget content, doubled card frames, a stray horizontal scrollbar, and clipped list rows, and caps the advertisers list so it scrolls within the widget instead of running the whole surface tall.
## 5.400.0 — September 8, 2026 at 3:13 PM UTC
* Sellers can now use the new Buyers tab, between Advertisers and Agents, to find existing external buyer relationships and their media buys, synced creatives, and account setup. No action is required: this reorganizes seller-only relationship work and does not change what counterparties see.
* Buyers can now author a social creative through `save_creative` without uploading a file; existing calls are unchanged. Pass a `social` block with `headline`, `body`, `description`, `callToAction`, and `displayName`, plus `components` for any slot a specific platform format declares (TikTok's `ad_text`, for example; a format that needs a Facebook Page or Instagram-account identity exposes it as a slot the same way). You still set `clickUrl` and a `formatKind` such as `image` or `video_hosted`, because copy alone carries no format identity. The copy lands in the standard AdCP text and URL slots, the same place a file upload would put it. `get` with `kind: "creative"` now returns the full record: ownership scope, canonical format identity and media kind, stored asset metadata, the authored copy read back from those slots, validation and sync state, and campaign membership. An advertiser-scoped `search` with `kind: "creative"` accepts `formatKind`, `assetType`, `role`, `source`, and `promoted` filters and reports each row's media kind and readiness; under campaign scope those filters are refused rather than ignored. Archiving frees the name for reuse and removes the creative from `get` and `search`; unarchiving is not available, and an `isArchived: false` request is refused rather than ignored.
* Attaching a creative to an existing package on a Meta-backed media buy through `update_media_buy` now fails with a clear, actionable error instead of a confusing "No update fields provided" rejection — Meta requires a separate `sync_creatives` call to actually deliver a creative, same as most other storefront adapters. `sync_creatives` also no longer silently returns an empty result for these media buys. If a creative attach to a Meta package fails, retry with `sync_creatives` as directed by the error message.
* Selected Agent details now keep Agent navigation and controls separate from seller workspace navigation.
* Reduce repeated capability checks and account-sync requests sent to active third-party sales agents, and reject placeholder `unknown` brand domains before they can be queued or sent to a seller. Internal discovery requests that identify an advertiser without repeating its brand now resolve the persisted brand instead of fabricating a fallback, so buyer discovery continues without repeating identity already stored for the advertiser. Account-change webhooks now return a retryable error when reconciliation cannot be durably queued.
## 5.399.0 — September 8, 2026 at 11:57 AM UTC
* The Approvals widget now shows what a creative actually did, not just what its source looks like. For sellers in the observation cohort, the "Does" line reads from the headless run Interchange made when the creative was submitted: the external domains it called, tracking pixels fired, redirects followed, popups and page navigations attempted, and media fetched, with when it was observed. While a run is still queued or running the row says so; when no run exists the line falls back to the source-derived summary and is marked "from source". The row stage also shows the observed thumbnail of the creative before you open the live preview. Observation is evidence from one run, never a safety verdict, and it never gates your decision.
* Selecting an advertiser now opens that advertiser's Campaigns workspace reliably for buyers and sellers, and the seller rail shows only that advertiser's jobs.
* Fixed creative sync failures against Storefront-backed sales agents (e.g. Snap) so the reported reason reflects the actual per-creative problem — a rejected review status, an unconfirmed package assignment, or a specific validation error — instead of an uninformative "Sales agent reported creative sync failure" for every kind of refusal.
* Rewarded-video and other product-discovery polls now receive a fresh wait budget for each request, so buyers can continue a provisional search until its storefront results settle.
## 5.398.0 — September 8, 2026 at 8:44 AM UTC
* The Sellers screen matches marketplace browsing: the market and channel filters now sit in compact controls instead of stretching across the page, and the market filter is searchable so you can jump straight to a country instead of scrolling the full list.
* Embedded sales agent 1.43.1: fail closed on invalid JSON and architecture drift, fail closed on unexecutable FreeWheel products, reconcile archived GAM inventory, traffic FreeWheel content inventory selectors, verify FreeWheel placement inventory contract.
* Nothing to change on your side. In your Advertisers tab the rail now starts with a Needs you count and one row per advertiser lens (House, Partner-managed, Self-serve, plus System when present) with how many advertisers each holds; the roster overview opens in the canvas, including for live storefronts. Selecting an advertiser shows a picker at the top of the rail and only that advertiser's jobs. Seller analytics stays under Operate → Dashboard.
## 5.396.1 — September 8, 2026 at 6:36 AM UTC
* Campaign `constraints.age_min`, `age_max`, `gender`, `genders`, and `demographics` now return a validation error instead of being silently accepted and dropped. These fields never affected targeting on any storefront — requests carrying them previously succeeded with no error and no effect. Describe age and gender targeting in your campaign brief instead.
## 5.396.0 — September 8, 2026 at 5:34 AM UTC
* If a media-buy request to a managed sales agent stops before the storefront starts sending it, it can be sent again. Once sending may have begun, the safeguard against duplicate media buys remains in place.
## 5.395.0 — September 8, 2026 at 5:01 AM UTC
* Creative review now includes live, interactive previews for every seller. In Approvals, select **Show preview** to run the buyer's creative at placement size in an isolated browser sandbox. The preview cannot access Interchange, your chat host, or your session. Source inspection and **Approve** or **Reject** remain available when a preview cannot be rendered. No setup is required.
## 5.394.1 — September 8, 2026 at 4:02 AM UTC
* Shaped by now names the past example your agent followed, on your pass and previews only; buyers never see it.
## 5.394.0 — September 8, 2026 at 2:51 AM UTC
* You can now add a deck, one-sheet, or PDF directly from the Library page.
## 5.393.0 — September 8, 2026 at 2:09 AM UTC
* Nothing to change on your side. In your Advertisers tab, selecting a buyer's advertiser now opens that buyer's activity with you: the media buys they placed, the creatives they synced, and their account setup. This works the same in Murph, Claude, and ChatGPT. It never shows the buyer's own campaigns. Delivery figures for those buys are not in this view yet; they remain in your Media buys and Reporting.
## 5.392.0 — September 8, 2026 at 1:32 AM UTC
* Buyer-agent developers can validate conversational MCP integrations against one portable contract. It defines `chat` and `get_conversation`, including MCP Tasks for longer turns and MCP Apps widget data in `structuredContent`. Nothing changes in the Interchange UI yet: no buyer-agent runtime serves this profile until the server and edge-route slices land.
* This contract does not alter what buyers see about sellers, so sellers have no action or notification for this release. It has no separate price or billable event; packaging and monetization remain unchanged.
* The contract is not exposed from a runtime, so no feature flag is needed in this slice. Expansion requires separately reviewed server and edge-route slices with their own release plan; reverting this package addition removes the contract before any runtime can expose it.
* Acceptance is an independently runnable schema suite covering valid chat, conversation pagination, MCP Tasks handles, MCP Apps metadata, and rejected invalid input. Adoption begins when a runtime records a successful `chat` call and a client reads a conversation with `get_conversation`; neither event exists in this slice.
* Creative reviews now carry an `observation` and a `thumbnailUrl`. For sellers in the observation cohort, Interchange runs each newly submitted creative version once, headlessly, on its render origin and records what the creative actually did: the registrable domains it contacted, pixel-shaped requests, redirects, popups, attempts to navigate the top window, and bytes transferred. The review row's `observation` field carries that record with its status (`queued`, `running`, `completed`, `timed_out`, `failed`), and `thumbnailUrl` points at an authenticated PNG of the creative frame from that run, bounded to 600 px on its longest edge and served only to the storefront that owns the review. A resubmitted creative gets a fresh observation, and the previous version's picture stops being served. Observation never gates review, live preview, or decisions; a row without one simply reads `null`.
* When you open Seller Dashboard as the owner of an active Demo Storefront, you can review a Synthetic evaluation data view for the current lease generation. It shows terminal evaluation RFP facts, including RFP and turn IDs, response state, the demo profile disclosure, and lease expiration. This rehearsal evidence is not buyer demand, commercial outcomes, bookings, revenue, attribution, learned posture, delivery, or live performance. Resetting the demo or allowing it to expire removes prior evaluation evidence. Your standard Seller Dashboard analytics remain live-only, and nothing changes on your side.
* Slack DM tenant defaults now resolve only when one active account has the exact normalized, approved email domain and the user holds a current authoritative seat on it. The same protection applies to buyer and seller accounts, requires no customer action, and ignores existing cached defaults in favor of bounded recomputation from current authority.
* The Demand inbox and Proposal pass no longer describe a practice, draft, or imported response as sent to a buyer.
* Marking a whole deck reusable could make every slide reusable, including priced slides. Now only individual slides, pages, or sheets can be marked reusable, and priced ones stay excluded.
## 5.390.1 — September 7, 2026 at 9:22 PM UTC
* Campaign creative uploads now show and count every creative found in a tag sheet before the sheet is submitted. Nothing changes about how the original tag sheet is uploaded or trafficked.
## 5.390.0 — September 7, 2026 at 8:14 PM UTC
* Teach your Merchandising Agent with the proposals you’d send again. Endorse a brief-and-response pair and the agent draws on those pairs when composing future proposals — you can trace which pairs shaped each response. Reuse slides across proposals and close open Library requests by uploading material or asking Murph.
* Agent developers who retrieve Set Up a Campaign through the buyer MCP skill catalog can now pin version 1.1.0 and run a repeatable no-spend sandbox advertiser check before relying on it. This makes automated skill checks reproducible, while the unversioned skill link keeps returning the latest instructions. Ordinary buyers do not need to change how they set up campaigns, and sellers are unaffected.
## 5.389.1 — September 7, 2026 at 6:38 PM UTC
* A media buy now records the currency its delivery is reported in, so anything reading that buy knows the denomination instead of having to assume one. The currency comes from the report itself — the currency every package agrees on, or the report's top-level currency if the packages do not declare one. A report that names no currency anywhere leaves the field empty rather than guessing at one. Buys that were already delivering have their currency recovered from their packages.
* If your context agent prices packages for a media buy, its price block now uses that buy's reported delivery currency where one is known, instead of falling back to your campaign's rate-card currency.
## 5.389.0 — September 7, 2026 at 5:29 PM UTC
* Advertiser currency changes now update in place instead of rebuilding; clarification requests no longer appear as failures. Always pass `primaryCurrency` (an ISO 4217 code) at create time to avoid locking silently to USD: `save_advertiser` defaults to `'USD'` when `primaryCurrency` is omitted, and that currency locks after the first campaign is created. Changing an unlocked advertiser's currency is an ordinary update with `advertiserId` plus the new `primaryCurrency`. When `name` or `brand` is missing the tool returns a neutral `needs_input` result instead of an error. Sellers are not affected by these changes.
* The only customer-facing change is an optional buyer API guard; existing proposal requests are unchanged. Buyer agents can add `expectedSellerId` to `request_proposals` when automation must remain confined to one seller. The server rejects a fresh round if current buyer authority no longer resolves exclusively to that seller; the value can narrow access but never grant it. Sellers receive no new or changed requests from this precondition, so no seller action is needed.
* Enrolled sellers now open the canonical Material-backed Product marketing page from Teach and can open the exact Proposal Pass shaped by an accepted selling point.
* The Sellers page now loads Connections correctly when activation is deferred. Repeated failures are also flagged automatically for review. Buyers do not need to change anything, and seller storefront presentation is unchanged.
* Nothing to change on your side. Advertisers that Scope3 operates for compliance checks now appear as **System** in your Advertisers list and in the advertiser roster API. The seller UI does not offer actions for these rows; advertiser lifecycle, relationship, and campaign creation actions are unavailable.
* Sponsored buyers can now complete a sandbox media-buy test with their seller when advertiser-to-seller activation checks are enabled. No action is required: seller visibility and activation defaults for other buyers are unchanged.
* Enrolled organization administrators can open Organization in the rail to use Members, Billing & contract, and Accounts without leaving their current account. Each pane works on the parent organization. When the rollout flag is off, the existing gear and parent-account navigation are unchanged.
## 5.388.1 — September 7, 2026 at 2:48 PM UTC
* The Sellers page keeps loading when a connection is waiting for activation prerequisites. This does not change storefront readiness or how sellers are presented.
* Fixed: brief-mode product discovery (`discover_products` and `request_proposals`) was ignoring the `countries` filter whenever a brief didn't also mention the country in prose, causing 0 products or an unhelpful "no usable candidate" result across Snap, Meta, TikTok, LinkedIn, Google, Pinterest, Reddit, Spotify, and UniversalAds. The `countries` filter now takes priority over any country implied by the brief text. `request_proposals` failures also now include the seller's own explanation (e.g. a brief-mode clarification reason) instead of a generic message.
## 5.388.0 — September 7, 2026 at 1:50 PM UTC
* The advertiser status filter in a seller's details is clearer. When every advertiser has the same status, the filter chips are hidden — there's nothing to switch between — instead of showing a single chip that blanks the whole list when clicked. If a search or filter matches nothing, the list now says "No advertisers match your search or filters" rather than showing an empty area.
* Sellers you haven't connected yet no longer look broken. A seller you've never connected now reads as a calm "Not connected" with a plain Connect action, instead of an amber "Action required" alert — and the "Action required" count reflects only connections that genuinely need attention (reconnect, account mapping, or billing).
* Published the Demo Environment Notice and linked it from Demo Storefront and developer documentation. Bring-your-own buyer agents now receive the same canonical notice URL with the visible and machine-readable warning on every demo/onboarding MCP tool result.
## 5.387.0 — September 7, 2026 at 12:28 PM UTC
* Browsing sellers is easier to scan: the channel and market filters now sit in compact controls instead of stretching across the page, and the market filter is searchable so you can jump straight to a country instead of scrolling the full list.
* Seller logos now render in the Sellers widget. When you browse or connect to sellers in an agent, each seller shows its brand logo instead of a placeholder with initials. Nothing to change on your side.
* Organization administrators now manage their organization's members, invitations, billing, and accounts from an in-place Organization section opened from a child account.
## 5.386.0 — September 7, 2026 at 11:31 AM UTC
* Meta delivery reports now accurately show the reporting metrics available for each campaign goal. They no longer publish inconsistent qualified-lead totals when package and campaign evidence disagree: a reported zero remains zero, missing lead evidence stays unavailable, and qualified leads are not shown as generic leads or conversions. Existing buyer setup and optimization are unchanged, and buyers do not need to take action. Seller setup and workflows are unchanged; this buyer reporting update does not apply to sellers.
## 5.385.0 — September 7, 2026 at 10:06 AM UTC
* Sponsored self-serve advertisers now open their own Campaigns work from the seller Advertisers tab, while external and delegated advertiser relationships remain unavailable there. This update provides read access only; write actions from that Campaigns Page are not yet available.
* Buyer organizations can now manage software Agents as first-class resources alongside Advertisers. The redesigned **Agents** collection shows each agent's purpose, connection state, advertiser access, and useful activity, while its detail experience organizes work into Overview, Access, Connections, Activity, and Settings.
* Named seller-account discovery replaces numeric ID entry. Connections distinguish Interchange, reporting resources, and machine subscriptions; activity keeps seller updates separate from exact human administration records. Technical identifiers, protocol versions, configuration generations, and proof state move to collapsed Advanced diagnostics.
* Settings retains one-time credential handling and exact-name confirmation for pause, retirement, credential rotation, revocation, and supported connection controls. The console continues to report **No advertiser access** until a separate authoritative grant exists; it never infers spend, payment, or Terms authority from identity, credentials, or connections.
* This workspace is for administrators and teams that already operate buyer automations. It serves power buyers and large-agency administrators; it does not change enterprise governance or the novice/chat buying experience. Existing integrations require no migration or customer action.
* There is no pricing, entitlement, packaging, or billable-unit impact. The workspace ships behind the existing direct organization-administrator authorization boundary rather than a separate feature flag, because its controls and visibility must share one authorization path. Expansion requires the documented cross-organization, revocation, zero-authority, and lifecycle checks; the kill switch is an atomic rollback of the workspace and named discovery route without changing existing authentication.
* Adoption is measured by buyer organizations reviewing an active Agent, usage by successful Add agent and administration flows, and guardrails by authorization refusals, control failures, post-revocation use, and any inferred authority. The owner reviews these signals at 30, 60, and 90 days using the plan in the commercialization brief.
* See [Buyer-agent credentials](https://docs.interchange.io/v2/setup/buyer-agent-credentials).
* Enhanced Reporting now consumes its 4-IU account-period activity only after the exact connected account completes a successful reporting sync. Enabling an account without a reporting subscription no longer creates IU usage.
* Connections now disables unavailable Enhanced Reporting controls until the applicable IU Rate Card is active and accepted, rather than showing a predictable enablement error.
* Delivery reads no longer report a combined spend figure that no single currency covers. When one media buy is served by several sources, `get_media_buy_delivery` previously added up every source's spend and labelled the total with whichever source happened to answer last — so \$200 plus €300 came back as one number under one currency, and money a source reported without naming a currency was quietly counted under another source's. It now withholds the figures it cannot state truthfully: the response omits `currency`, `aggregated_totals`, and the `spend` inside `media_buy_deliveries[].totals`, and says why in `errors[]` — `MIXED_CURRENCY_DELIVERY` when sources named different currencies, `SPEND_DENOMINATION_UNRESOLVED` when a source reported spend without naming one. Impressions, per-source status, and every other non-monetary metric still report as before, and a buy whose sources share one currency is unchanged. Read each source separately to see its amount in its own currency.
* Storefront connections no longer claim that advertiser provisioning is underway when setup has been safely deferred. Activation failures now also produce explicit operational evidence for faster diagnosis.
## 5.384.1 — September 7, 2026 at 4:56 AM UTC
* Organization administrators can again find Members, Billing & contract, and Accounts in the seller rail while the setup checklist is shown.
## 5.384.0 — September 7, 2026 at 4:14 AM UTC
* Organization administrators can open Members, Billing & contract, and Accounts from the rail without switching away from their current account. Nothing needs to change on your side.
* Seller impact: not applicable. This signed-in organization navigation change does not alter storefronts, inventory, seller presentation, or anything buyers see about sellers.
* Monetization: none. This changes where existing organization settings open; it does not change billing plans, packaging, prices, entitlements, or charges.
* Buyers now live under Advertisers: each buyer is an External advertiser row, and pending ones show as 'awaiting you'.
## 5.383.1 — September 7, 2026 at 2:27 AM UTC
* Connecting a provider account (TikTok, Meta, and other OAuth-based storefronts) from a connection link now reaches the provider's sign-in page. Clicking Continue no longer silently stalls in some browsers before the sign-in screen ever loads.
## 5.383.0 — September 7, 2026 at 2:00 AM UTC
* Sellers who have not connected an inventory source yet see the setup checklist again.
* Advertiser updates, tracking configuration changes, and archiving through the Buyer API now require the same current Buyer identity, plan, Terms, and account-hold checks as advertiser creation.
* Delivery spend now books in the currency your packages declare, not the one the report's top-level field claims. Some sales agents send a delivery report whose overall currency contradicts every package inside it — those deliveries previously failed to book at all and sat unbooked while the mismatch was chased down. Where the packages agree with each other, that shared currency is now what we book and what we report, and the contradiction is flagged so the source can be corrected. Reports with genuinely mixed package currencies are unchanged, and a report that declares no currency anywhere is still never booked against a guessed one.
* The Advertisers tab now opens on your organization's full roster instead of a chat prompt. It reflects your existing advertisers automatically — nothing to set up on your side.
* Each row is one of three kinds: House (you operate it), Self-serve (your client runs it, though you can still edit), or External (a buyer connected through their own agent). A Sandbox badge marks any test advertiser and stays hidden behind a toggle. Filter the roster by House, Self-serve, Partner-managed — the same External rows, filtered — or Needs you, which shows every row with an open setup case waiting on your response.
* House and Self-serve rows open Campaigns, Creatives, and Reporting. External rows open Account — the same setup-case drawer Buyer Account Mapping already shows (billing path, setup requests, inventory mappings), so it's one place to review instead of two.
* This only changes what you see on your own Advertisers tab. Buyers see no change: the Account drawer is your existing Buyer Account Mapping case, reached from a new place but showing no new buyer-facing data.
* Enable Enhanced Reporting for an active connected provider account when you want up to one year of reporting history for that account. If you do nothing, your standard reporting, connections, campaigns, and charges do not change.
* For a governed media organization, the account-level control makes each reporting and billing decision explicit. For hands-on teams, it is available in Connections, the API, and MCP; an account earns 4 Interchange Units per billing period only after its existing reporting subscription successfully syncs after the control is enabled. Turning it off stops future enhanced reporting while preserving the history already collected.
* Seller impact: not applicable. Sellers cannot see or configure this buyer-side account control; their storefronts and ordinary connection and campaign workflows do not change.
* TikTok product discovery now reports which account setup (app, pixel, connected identity) each candidate product still needs before you can execute a campaign with it — not only for the one product a decision plan already picked, but for every product returned from a brief-driven search.
## 5.381.1 — September 6, 2026 at 11:33 PM UTC
* Seller setup navigation is cleaner and easier to scan, with aligned status details and a compact go-live summary.
## 5.381.0 — September 6, 2026 at 11:00 PM UTC
* During seller signup, choose **Just list** to list inventory with an agent operated by you, a partner, or another provider, or **Create an AMC account** to list inventory and train your AMC's hosted Merchandising Agent. An AMC account always includes listing. Interchange uses that selection to show the setup workspaces and navigation that apply. Existing sellers do not need to act; use Account settings or ask Murph if you want to switch later. Switching does not change your plan or inventory sources and does not delete completed setup. The initial choice and canonical Storefront capabilities commit together, so onboarding cannot complete with stale workspace routing. This launch does not introduce new pricing, change a seller's plan, or grant an entitlement; it configures which already-packaged seller product the account is setting up.
* Measurement: track `media_company_intake_started` and `media_company_identity_confirmed`, then measure current-mode adoption from the canonical Storefront projection. Before expanding the flag, complete at least one onboarding in each mode and one switch in each direction with zero authority divergence and no sustained increase in confirmation errors.
## 5.380.0 — September 6, 2026 at 9:46 PM UTC
* During account signup, you can now record the exact product, country, and channel you need when that option is not ready. The waitlist applies to that request, so you can continue setting up work that Scope3 supports. See the Buyer onboarding and Market readiness guides for the available paths.
* For buyers, the Buyer Setup response now names the identity, plan, Terms, account-standing, product-access, or payment step blocking an action. Existing integrations need no response-parsing change: `platformReady` keeps its prior wire behavior, and developers can adopt `capabilities` from the Buyer Setup operation when they are ready. This is an intentional authorization cutover: an existing account that lacks verified Organization identity, a selected plan, or current Terms is denied real operations and receives that specific blocker; deprecated `LIVE_ELIGIBLE` posture does not bypass those requirements. Payment remains required only for purchases on routes that clear through Interchange.
* For sellers, signup can record interest for the country and channel where you want to offer supply. This does not change how your storefront ranks, presents inventory, sets prices, or becomes ready to transact. No action is required for an existing storefront.
## 5.379.0 — September 6, 2026 at 8:26 PM UTC
* Approval notifications now open straight to the review waiting on you. The email for a pending creative review names the creative, the buyer, its format, and its size when the creative states one, plus a **Review creative** button that opens directly on that review. A pending media-buy approval works the same way, with a **Review media buy** button. The in-app feed entry and the Slack post's **Open in Interchange** link open to the same review. Nothing to change on your side: delivery still follows your storefront's approval routing rules, and notifications never include the creative itself or a preview link, since the live preview is created only when you open the review.
* V3 advertiser creation safely replays an uncertain response when callers reuse one idempotency key.
## 5.378.0 — September 6, 2026 at 6:03 PM UTC
* When a brief names an age range a Seller cannot target exactly, `get_products` and `request_proposals` now return a proposal instead of an error: products without age targeting, plus a `limitations` entry (`age_targeting_unavailable`) that states the requested range, the ranges the Seller can target, and any fixed-range inventory that was excluded. A brief that names disjoint ranges (for example 21-24 or 35-44) is asked to clarify rather than answered for one segment. Only when the Seller has no inventory without age targeting does the request still fail as before. Buyer integrations that read `get_products` programmatically should check `limitations` before treating a proposal as fully on-brief; agents that read the proposal text need no change. Sellers see the same limitation in the run record and can declare the age ranges their inventory targets (seller-authored age signals) so briefs that name them are answered exactly.
## 5.377.0 — September 6, 2026 at 5:03 PM UTC
* Sellers with more than one storefront can now open the proposal pass returned by a practice brief from their selected storefront. Practice briefs now use the same session-selected storefront context as the proposal-pass view.
* When you ask Murph to set up or manage your Seller Account in a private conversation, Murph now handles the request with dedicated seller actions. Murph remains restricted in shared conversations.
* You can now open the Advertisers page from your seller workspace to see every advertiser in your organization's roster. Each row is labeled House, Partner-managed, or Self-serve. House is an advertiser you run directly, live or sandbox. Partner-managed rows are run by a connected partner — an AdCP counterparty or a delegated relationship. An advertiser you've set to self-serve mode shows as Self-serve. When someone else operates the advertiser, the row also shows the operator's name.
* A row with an open setup step you need to act on shows "Awaiting your decision," "Waiting on buyer," or "Awaiting billing," with a Review setup action that opens the same case in Seller Accounts.
* Your Playbook now opens with a first-run checklist of what your Merchandising Agent sells from: business rules and a rate card (required), plus a media kit, selling guidance, and past proposals or sales decks (recommended), each with a line on why it sharpens the agent's pitch. Every buyer brief the agent has answered now reads in four blocks (what the buyer asked, what we responded, outcome, our read), and the Inventory components and Signals pages open by explaining how the agent uses them; nothing to change on your side.
## 5.376.0 — September 6, 2026 at 2:13 PM UTC
* Creative review now shows the ad itself. When you open a pending creative in Approvals, **Show preview** runs the buyer's creative live at placement size: third-party display tags, hosted HTML, VAST video, DAAST audio, images, video, and audio all render, animate, and respond to hover and click the way they would on the page. **Open full size** shows the same preview in a new tab through your chat host.
* The preview runs on Interchange's dedicated render origin (`render.interchange.io`), inside a browser sandbox that has no access to Interchange, your chat host, or your session. Click macros are rewritten to an interstitial that shows the destination instead of leaving the page; a tag's own code can still open a new window, which stays sandboxed: it cannot script or read your page or read cookies and storage itself, though like any navigation it reaches that site with the cookies your browser already holds for it. Treat any window the preview opens as buyer-controlled. Each review row carries a signed preview link that expires after fifteen minutes and stops working the moment the buyer resubmits the creative or the review is decided.
* Seeing a creative run is not a safety verdict. The trust chip, the plain-language "Does" line, referenced domains, and the source itself stay beside the preview so you can judge what the tag does as well as how it looks.
* Buyers: nothing changes in how your creative is delivered. During review a seller may render your submitted creative in this preview, where its tags and trackers fire as they would on a page and its click macros resolve to an interstitial rather than a landing page. No action is needed.
* This capability is included in creative review at no additional charge: no package, entitlement, usage charge, or other billable surface is added.
* Adoption is measured through the fixed lifecycle events `mcp_creative_preview.live_preview_open`, `.live_preview_full_size`, and `.live_preview_expired`, alongside the existing `.request`, `.returned_link`, `.external_open`, and `.expired_link` events: preview-open rate per pending review, full-size rate per open, and the share of links that expire unused. These events carry no creative, URL, provider, or buyer identifiers.
* Creative review rows in Approvals now lead with the creative instead of the caveats. Each row opens to what the buyer sent: an image + copy creative shows its headline, body, and call to action as text; a hosted file shows its format, size, and host; a third-party tag shows who serves it and, in plain words, what it does (loads a script, fetches VAST from a domain, fires tracking pixels). One chip in the row header names the trust level, the tag source sits behind a "View tag source" disclosure, and Approve and Reject are in the same place on every row. Where an interactive preview is available, the button names the provider it will send the creative to, and its expiry is shown in your locale.
* Nothing about the safety boundary changed: buyer HTML, JavaScript, VAST, and media URLs are still never loaded or executed inside Interchange, and the provider preview still opens only through your chat host after you ask for it.
* Agent-supplied Listing storefronts now show their connected-agent account overview instead of unrelated Interchange merchandising analytics. Branding & distribution now presents a clear Public Distribution upgrade path before exposing paid hostname, self-serve advertiser, AdCP, and ChatGPT app controls.
* ### Enterprise brand buyers
* Connected-account campaign tracking now works without a separate enrollment, keeping campaign identity, governance, and live delivery evidence in the same Interchange workflow. Existing subscriptions need no changes, and provider capability and account-access checks still apply.
* ### Agency buyers
* After mapping and subscribing an eligible provider account, teams can inspect its mirrored campaigns, account presence, and live delivery across advertisers without waiting for feature enrollment. Tracking remains read-only and does not grant authority to change provider campaigns.
* ### Builders and power buyers
* Existing integrations continue to use the buyer API or `subscribe_directed_campaigns` in MCP. The former feature-enrollment error is gone; account mapping, credentials, provider capabilities, and advertiser access remain the operative checks.
* ### Buyers using chat
* No technical setup or action is required. Once an eligible connected account is mapped and subscribed, its campaign history and current delivery become available through the normal campaign experience.
* ### Sellers
* Listing + Distribution now makes a storefront easier for buyer agents to discover and transact with by publishing a stable `/adcp/mcp` action in its machine-readable Listing. Keeping the Seller CNAME verified gives buyers a branded, predictable route into the storefront's governed campaign, contract, reporting, and billing workflow. Other Seller profiles remain private from this transaction endpoint.
* This release does not introduce a new charge or billable unit. Listing + Distribution remains the existing packaging boundary for the Seller endpoint, while connected-account campaign tracking remains a buyer capability.
## 5.375.0 — September 6, 2026 at 10:23 AM UTC
* Review & go live now preserves the standard not-found response when a campaign is outside the active buyer account, regardless of that account's Campaigns Page availability.
## 5.374.2 — September 6, 2026 at 5:00 AM UTC
* Agency trading teams and in-house media teams enrolled in Campaigns V3 can move from an advertiser to its campaigns in Claude or ChatGPT, review campaign status, budgets, media buys, and creatives, and open Review & go live without losing their place. Hands-on buyers using compatible external hosts get the same workflow. No setup change is required.
* Monetization: none. This improves the existing buyer workflow and adds no charge.
* Rollout: `campaigns-v3` remains the registered customer gate. Expand after Campaigns Page opens, campaign loads, and advertiser-to-campaign navigation remain stable. Disabling it returns buyers to the current Campaigns view; unavailable evaluation fails closed to that view.
* Measurement: monitor Campaigns Page load and retry rates, advertiser-to-campaign navigation, campaign drill-in and Review & go live opens, and use by enrolled customers.
## 5.374.1 — September 6, 2026 at 4:22 AM UTC
* Buyers using the MCP `get_products` tool or the Product Discovery API at `POST /api/v2/buyer/products/query`: when an initial response says results are still in progress and you continue the same discovery, the completed discovery results now remain available even during a temporary cache outage. You also see each refinement result only on the product or proposal from the seller that returned it. How you start a new discovery and its request fields are unchanged; continue with the same execution ID through the MCP tool or API endpoint, with no setup change required. When refining or applying a result, send back the exact ID you received. A bare product ID shared by two of a seller's Inventory Sources is rejected as ambiguous, so use the returned qualified product ID. Proposals duplicated across those Sources are withheld until the seller returns distinct proposal IDs.
* Sellers: This fixes a display issue triggered when two Inventory Sources in one Storefront return the same proposal ID. Buyer-facing MCP and API discovery results now show only proposals that can be attributed to one Inventory Source, so distinct proposal IDs receive reliable buyer visibility once the Storefront finishes responding instead of risking the wrong proposal being displayed. Existing unique proposal IDs need no change; if you reuse an ID across Inventory Sources, update the proposal IDs returned by those Sources to keep every proposal eligible for display.
## 5.374.0 — September 6, 2026 at 2:34 AM UTC
* Child-account members can use `GET /api/v2/billing/account/inherited-stub` to show that billing is managed by their organization without exposing its billing document. The response contains only the organization name, organization scope, and managing customer pointer; full billing access and advertiser, API key, Agent, workload, and service-token restrictions are unchanged.
* For enterprise brand and agency buying teams that work from child accounts, an API integration can now explain that billing is managed centrally instead of requesting or inferring the parent billing document. Existing integrations do not change unless they call this new endpoint. Genuine seller child-account members can use the same shared read under the same membership and topology rules; it does not change Storefront billing workflows or what buyers see about a seller.
* This is an additive infrastructure read projection, not a new billable surface, plan, package, entitlement, or charge. It creates no billing write and exposes no contract, rate, standing, payment, account, member, or advertiser data.
* The endpoint ships without a feature flag because this server slice adds no UI entry point or discovery and remains behind existing authentication plus its strict direct-child membership checks. The later in-place rail UI is separately PostHog-gated. There is no endpoint-specific runtime kill switch; rollback is a server deployment revert. UI exposure expands only after the server soak has zero unexpected `5xx` responses and the exact `200` and denied `401`/`403` contracts remain verified.
* Existing route-level request counts and latency by status and customer, plus durable REST activity records for `GET /api/v2/billing/account/inherited-stub`, measure successful, denied, and failed calls without a new event. For this slice, usage is a successful request by an eligible child account; acceptance is zero unexpected `5xx` responses before UI rollout. UI adoption is not claimed or measurable from this server-only endpoint: the later flag-gated rail slice must define and use its actual instrumented exposure and usage signal.
* Show the next required setup action in the seller navigation header whenever setup remains incomplete.
* AI Business Rules now recheck storefront profile access under the same lock as profile changes, preventing a simultaneous profile removal from allowing a stale rule creation or activation to commit.
* Buyer agent principal-change notifications now retain one delivery identity across retries, preventing duplicate notifications after a delivery timeout. No action is required.
* Buyer agent principal package types now include the supported reporting ledger-change notification event.
* On the Branding & distribution Page, you now choose Interchange or Public for Distribution — there's no separate "Hidden" option. A storefront that hasn't published yet shows as "Not listed yet," a state, not a third choice to pick.
* Choosing Public now walks you through what's needed: a public listing domain card at the top of Distribution shows the CNAME to point at your own domain. Add it, and select Verify once your DNS has updated — once your domain verifies, select Public to go live (verifying confirms your domain is ready; it doesn't switch Distribution for you). You'll need a published Interchange listing before you can verify.
* Storefronts already publishing to Interchange are unaffected. No storefront besides Interchange's own could choose Public before this change, so nothing to migrate. Interchange's own first-party storefront keeps its platform-managed setup automatically — it never shows the domain card.
* Want to take your storefront off Interchange and the public web entirely? Select **Unlist** below the two-way choice and confirm — your storefront returns to "Not listed yet" until you publish again. Unlist is a separate action, not a third distribution choice, and it's the same effect the old "Hidden" (`private`) value always had — that value still works for any tool that sends it directly.
* AI Business Rules now consistently names the seller Page for policy and approval decisions.
## 5.373.0 — September 5, 2026 at 9:22 PM UTC
* In the Advertisers tab, select the Partner-managed or Self-serve lens to see which buyer relationships require setup. The roster marks relationships as Needs setup — waiting on you, Waiting on buyer, or Awaiting billing; select Review setup to open the same setup case drawer available from Buyer Account Mapping. House rows carry no setup state. Nothing a buyer sees about you changes: the states are derived from setup cases already visible to the seller in the Buyers directory, and no buyer-facing response or page is altered.
* Buyer-organization administrators can now use the buyer REST API to see how a buyer agent is configured, whether its notification and reporting setup has been proved, and what still blocks readiness. Registration, an active credential, configuration proof, advertised capabilities, and business authority remain separate checks. Passing one check never grants advertiser, account, spending, payment, or Terms access. Existing integrations do not need to change, and the configuration UI remains withheld until this API contract is stable.
* Administrators can deactivate one notification subscriber or reporting destination, suspend a buyer agent, or permanently retire it. Each control requires the buyer agent's exact display name and current state or version. The administration history records the authenticated person who made each change without storing endpoint details, proof material, or secrets.
* Seller-recorded changes appear in a separate `principal.changed` history and tell the buyer agent to refresh with `get_principal`. The buyer agent's own successful `sync_principal` calls do not appear as incoming seller activity. Both histories return the latest 100 records and say when older records exist.
* Seller impact: sellers do not need to act, and buyers see no new seller ranking, inventory, packaging, or readiness information. Sellers continue to enforce the buyer agent's configured lifecycle, while buyer agents can see when a seller records a proof or destination-status change. Monetization impact: none. This adds no billable surface, entitlement, price, Interchange Unit, spending, payment, or Terms authority.
* Availability is limited to directly authenticated organization administrators through the buyer REST API. That server-side authorization check is the release gate; rollback removes these administration routes together. Wider use requires the authorization, organization-isolation, destructive-control, notification-recovery, and authenticated-route checks to pass. Adoption is measured from successful administration audit records. Guardrails track rejected authorization, failed controls, notification-loss alerts, and any unexpected authority response without collecting endpoints, destinations, proof material, or secrets.
* See [Buyer-agent credentials](https://docs.interchange.io/v2/setup/buyer-agent-credentials#inspect-configuration-proof-and-readiness).
* Open Agents in Murph and select an Agent to manage its endpoint, certification, and inventory sources from the left rail. First-party Agents omit client onboarding controls; client-serving Agents show them when authorized. Open an exact inventory source for credentials, connection tests, and diagnostics. Existing connections need no setup changes.
## 5.372.0 — September 5, 2026 at 7:58 PM UTC
* Seller Accounts on Listing can review campaigns, media buys, creatives, approvals, delivery, activity, and reporting in Interchange while a connected sales agent handles selling. The current Campaigns experience and AI Business Rules become available when the account moves to Listing; no action is required to keep using the connected agent, and saving, enabling, or evaluating AI Business Rules is not IU-rated today. To add self-serve advertisers, public listing distribution with an optional CNAME, or customer-branded AdCP and ChatGPT app channels, choose Listing + Distribution in Plan & billing. This paid package does not change the visibility or ranking of the Interchange listing and does not enable Merchandising. Buyers do not see the seller's account-profile label, and their existing Campaigns V3 exposure remains unchanged.
* Certified plan intent is pre-release and is not yet available to Seller Accounts. The `seller-rfp-plan-intent` flag remains disabled until the follow-up trigger-enforcement migration ships, so RFP responses retain the legacy contract. The pre-release capability adds no entitlement, package, or billing meter.
* There is no Seller Account action to take for this pre-release foundation.
* If your buyer agent or integration creates media buys from Buyer Proposal JSON, preserve each `productId` and `pricingOptionId` exactly as returned when calling `create_media_buy`. Buyer Proposal JSON keeps its published shape and field set, but these values now include their source so identical upstream IDs from different sources stay distinct.
* Integrations that already pass returned IDs through unchanged need no code changes. Existing defaults are unchanged. Integrations that map or replace IDs should instead preserve the returned values exactly.
## 5.371.0 — September 5, 2026 at 1:22 PM UTC
* Preview CSV, XLS, and XLSX tag sheets locally before upload with the new Inspect a Tag Sheet skill. The offline inspector shows the creative-row count and versioned mapping rules without uploading or printing tag content. When a readable tag-sheet shape uploaded through Murph repeatedly produces no creative rows across customers, Interchange sends one deduplicated, content-free signal for team review; nothing else is required from you.
* Route each seller setup checklist item and its next action to the incomplete task that needs attention.
* Sellers can use the left rail in their Seller Account to move between Inventory, Advertisers, and Agents. Setup progress stays in Inventory, and campaigns stay with each advertiser.
* Use `open_proposal_pass({ rfpId, turnId })` in the v3 Seller Account MCP surface to open the Proposal Pass Page for an exact RFP turn. The launch returns only those IDs; the Page retrieves private proposal detail after it opens. Invalid pairs and records outside your account do not open, and the existing v2 storefront launch remains unchanged.
## 5.369.1 — September 5, 2026 at 7:55 AM UTC
* Creatives delivered after storefront review now preserve the downstream platform creative ID in the terminal buyer callback, so provider materialization appears in creative readback. Buyer-visible provenance is also correctly attributed to the seller storefront rather than its private inventory source.
## 5.369.0 — September 5, 2026 at 6:56 AM UTC
* Creatives on approved future-flight media buys now receive their package binding while the buy is pending start, so delivery no longer waits for the flight to become active.
* Source Diagnostics now explains recognized request-field failures in plain language, shows actionable fields and protocol guidance, and keeps the complete redacted upstream response in collapsed technical details.
* Creatives attached while a seller is reviewing a media buy are now delivered automatically after approval. Buyers no longer need to detach or resubmit them.
## 5.368.0 — September 5, 2026 at 3:32 AM UTC
* Inventory Sources is now available from the navigation in every seller account. Use it to connect an AdCP-compatible sales agent and open each Source's settings, credentials, connection tests, and diagnostics. The agent may be operated by your company or by a provider; that choice does not require a different plan.
* No action is required for existing Sources.
* Commercial model: none. Connecting and managing an inventory Source remains included in the free Listing-only setup. Paid Merchandising controls still require a Merchandising plan and its entitlements; this navigation rollout does not change prices or grant paid access.
* Persona impact: seller operators get one consistent place to manage Sources. Enterprise brand, agency, power, and small-business buyers have no new action and see no change from this navigation release.
* Buyer-visible effect: none from the navigation rollout itself. Buyers continue to call the same storefront, and Source eligibility and returned products are unchanged until a seller configures or changes a Source.
* New seller workspaces now start with the tools included in the option selected during signup. Choose Listing only for the free publishing setup, or choose a Merchandising plan for richer product and inventory-management tools. There is no separate support request or hidden account switch after signup.
* Your workspace stays aligned when you upgrade, renew, downgrade, or join an approved beta. A setup change is applied only when every included capability is available, so a failed change cannot leave the account partly updated. The current setup is visible in the account switcher.
* Commercial model: Listing only is the free connection and publishing setup. Merchandising is the paid package for modular inventory and self-service publishing. The accepted Rate Card remains the authority for price and product rights; a feature profile only presents those rights and cannot grant or price anything by itself.
* Persona impact: seller operators choose the setup that matches how they sell. Enterprise brand and agency buyers may discover richer products after a seller chooses Merchandising. Power and small-business buyers use the same storefront interface and do not need to change their workflow.
* Buyer-visible effect: Listing-only does not change how buyers access a seller's storefront. A seller that selects a Merchandising plan can publish the richer products enabled by those paid capabilities, which buyers may then discover through the existing storefront interface.
## 5.366.1 — September 5, 2026 at 1:37 AM UTC
* Managed sales-agent health now recovers after a successful status refresh instead of retaining a transient unreachable warning for seven days.
## 5.366.0 — September 5, 2026 at 12:59 AM UTC
* Source diagnostics can now launch deterministic validation for connected third-party sales agents instead of stopping before validation reaches the agent.
* Preserve package identity in newly generated click and impression tracking when a creative has exactly one package assignment.
* Seller home now starts eligible storefronts at **Interview your Merchandising Agent**. Choose **Teach** to add and confirm seller material, or **Test** to practice a buyer brief. Existing users do not need to change their setup, and each workflow keeps its existing access, review, and evidence requirements.
* FreeWheel media buys now create active placements with the selected inventory, retain the validated currency and connection settings through booking, and attach creatives immediately. No seller action is required.
## 5.365.0 — September 4, 2026 at 11:38 PM UTC
* External Agent setup now uses shorter, ownership-neutral guidance, and Source Diagnostics opens on a live Source by default while keeping inactive Sources available for deliberate review.
## 5.364.0 — September 4, 2026 at 10:33 PM UTC
* Buyer Storefront connection reads now include an opaque selection-edge revision, allowing read-only clients to verify repeated selection results while treating revision bumps as fencing changes rather than seller-choice changes.
* Campaign creative uploads now accept CM360 tag sheets in CSV, XLS, and XLSX formats, keep their placement tags ready for review, and let you search all managed campaigns by name when choosing where to use creative.
* Buyer-agent sandboxes that retain their first eligible provider account now remain active when registration expiry cleanup runs concurrently, including during a rolling deployment with an older API or worker. Cleanup still removes truly expired, unbound sandboxes and safely retries interrupted Core deactivation.
* Buyers now see regional market makers under "Selected & default-on" on the Sellers page, and regional market makers with no declared regions join automatically for advertisers targeting their approved markets.
* You can now filter the Sellers page by market and channel, so you can find, for example, every seller selling in the Netherlands or every seller offering CTV. The filters combine with search and the existing status filters. In the web app, share a link with `market` and `channel` in the URL to open the page with those filters already applied.
## 5.363.0 — September 4, 2026 at 7:31 PM UTC
* Restore seller billing links and make empty advertiser and collapsed-widget states recoverable in the seller rail.
* Find approvals and other pending seller work in one **Approvals & operations** rail destination, with approval routing still reachable from the combined inbox.
* Keep the Agents workspace inside Murph so sellers can browse and create Agents without losing their chats or composer.
* If you're invited to administer a new Buyer Account, use the Welcome to Interchange email to join the organization and review any outstanding Terms of Service. If your organization already has a Seller Account, it remains available as a separate workspace; no changes are required to keep using it.
## 5.362.0 — September 4, 2026 at 4:42 PM UTC
* Plan & Billing now presents a Rate Card—the activities and prices included in a plan—with three Intelligence Unit activities for new v4 plans: Brief responses, Social account intelligence, and non-social Interchange media buys that record delivery. Scope3 initiates the transition by offering a v4 plan, and an admin for the organization must explicitly accept it. No customer is migrated automatically; existing v0-v3 plans and historical usage remain unchanged.
* **Enterprise brand buyers:** compare the three outcome-based rates with your governance and measurement plan before an admin accepts the offer.
* **Large agencies and hold-co buyers:** update pricing playbooks only for organizations that choose v4; current client plans need no action.
* **Hands-on buyers:** Plan & Billing shows the exact activity, unit, and rate before acceptance, so experiments can be costed against the same schedule the meter uses.
* **SMB and chat buyers:** nothing changes unless your organization is offered and accepts a new plan; Murph and Plan & Billing will present the new schedule.
* **Sellers:** a v4 plan rewards complete storefront responses and trustworthy channel attribution. Only sellers that explicitly accept v4 may incur one IU for a non-social Interchange media buy that records impressions or spend in a billing period; existing seller plans and earlier buys are unchanged.
* This release uses **Social account intelligence** as the customer-facing name for eligible social-account access. Usage is charged only after the matching customer outcome occurs. A non-social media buy costs one IU in a billing period only when Scope3 records positive impressions or spend for that buy in that period. If corrected delivery data returns both totals to zero before the invoice, the IU is removed. If the correction arrives after the invoice, Scope3 applies the credit or reversal to the original period; the customer does not need to take action.
* There is no separate feature flag: customer exposure is structurally gated by an offered and explicitly accepted v4 plan, and rollout coordinators will not create v4 offers until governance PR #12746 is approved. The immediate kill switch is to stop issuing v4 offers; accepted Rate Cards remain immutable and continue to follow their signed terms. Expand beyond the initial offers only after the first accepted v4 billing periods show zero unresolved attribution candidates, zero duplicate metering keys, and successful recovery sweeps for all three activities. Rollout coordinators will monitor accepted-v4 counts, per-activity metering events, recovery failures, unresolved attribution, and duplicate suppression in the launch tracker and `project-iu-rollout` channel.
* Murph now keeps uploaded creative tag sheets available while buyers choose a campaign in a later message, instead of asking them to upload the same sheet again.
* Creative review rows in Approvals now lead with the creative instead of the caveats. Each row opens to what the buyer sent: an image + copy creative shows its headline, body, and call to action as text; a hosted file shows its format, size, and host; a third-party tag shows who serves it and, in plain words, what it does (loads a script, fetches VAST from a domain, fires tracking pixels). One chip in the row header names the trust level, the tag source sits behind a "View tag source" disclosure, and Approve and Reject are in the same place on every row. Where an interactive preview is available, the button names the provider it will send the creative to, and its expiry is shown in your locale.
* Nothing about the safety boundary changed: buyer HTML, JavaScript, VAST, and media URLs are still never loaded or executed inside Interchange, and the provider preview still opens only through your chat host after you ask for it.
* You can now understand billing figures, update Business Rules from the top of the page, and diagnose ad-server sync failures without hunting across duplicate status panels. Approvals also adds a buyer filter and lists recently approved media buys with the newest decision first. Nothing needs to change on your side.
* The signup chooser appears only when Scope3 enrolls you. You can choose Advertiser, Advertiser Portfolio, Media Company, or Developer. An enrolled Media Company signup then follows the existing seller flow. Seller presentation remains unchanged when enrollment is not enabled.
* Your choice only determines your signup experience. It grants no authority or access and does not change billing, navigation, or capabilities. Nothing to change unless you are enrolled.
## 5.361.0 — September 4, 2026 at 1:24 PM UTC
* Keep the setup checklist visible after you connect an external sales agent, including while its product mappings and agent link are still being established.
* Fixes media buys that could get stuck in DRAFT status when a storefront (e.g. PubX) calls back with an acceptance before our system finishes writing the correlation record. The webhook handler now retries the lookup briefly to close that window, so buys transition to ACTIVE as expected.
* Modular inventory catalog refreshes now preserve a source-declared guaranteed or non-guaranteed delivery type. This keeps buyer cancellation behavior stable when a static-avails product is regenerated from its backing source.
## 5.360.0 — September 4, 2026 at 12:23 PM UTC
* Fixed TikTok Video Views (6-second and 15-second) media buys failing readback verification and being rolled back after creation. TikTok auto-populates the ad group's conversion tracking event to match its optimization goal even when none was requested; the adapter now tolerates that provider-side normalization instead of treating it as a mismatch.
* Approved Media Company applicants can now review a research-backed setup draft in the Murph onboarding rail. The draft surfaces inferred operating role, candidate properties and domains, markets and channels, likely inventory sources, and selling-path guidance — helping you configure your Storefront before your team reviews it.
* Each field is labeled INFERRED, APPLICANT\_CONFIRMED, or INDEPENDENTLY\_VERIFIED so you know which suggestions to validate. Settlement currency is a suggestion only; set it in Plan & Billing. adagents.json is a draft for your review; you publish it yourself when ready.
* This has no buyer-facing behavior. It does not change pricing, packaging, billing, or paid-tier eligibility; it is an onboarding reliability and time-to-value improvement for enrolled sellers.
* Adoption is measured with `media_company_setup_draft_viewed`, emitted on a non-impersonated applicant read. For the asynchronous test cohort, the acceptance target is that every approved applicant with a completed projection can open the draft without assistance; after launch, track the share that reads it within seven days of approval and median time from approval to first read.
* Correct native adapter reporting mappings and declarations so cross-platform metrics preserve their standard meanings and supported conversion values are not dropped.
## 5.359.0 — September 4, 2026 at 10:47 AM UTC
* Keep the Inventory, Advertisers, and Agents workspace labels readable across the seller setup rail.
* Fixed `create_media_buy` on TikTok returning a generic, retryable "Upstream request failed" error for requests that were actually missing a required field (app ID, conversion event source, messaging-app destination, platform identity, or a Smart+ creative). These now surface as a correctable `INVALID_REQUEST` naming the missing field, so the buyer knows what to fix instead of retrying a request that will never succeed.
* Fixed TikTok Video Views (6-second and 15-second) media buys failing readback verification and being rolled back after creation. The ad group now requests the Video Views-specific promotion type TikTok requires, matching the fix already applied to TikTok Reach.
* Icons for your ChatGPT app now come from the logo in your brand.json — nothing to upload. If your storefront doesn't have a logo there yet, add one; your directory (256×256) and composer (48×48) icons generate automatically the next time you publish.
* A FreeWheel inventory source that was withheld from buyers because its execution currency had never been confirmed with the ad server now clears itself. Interchange reads your FreeWheel network, restates the execution currency already configured on the source, and the source becomes buyable — within fifteen minutes, with nothing for you to do. It never chooses the currency for you: FreeWheel stamps the currency on each insertion order itself, so Interchange only ever restates the one you configured. Two states still need you, and the **Ad-server execution currency** row on the source names which: credentials that cannot read your network's insertion orders, and a network whose existing orders book a different currency than the source declares. Google Ad Manager, SpringServe, and AdsWizz sources are unchanged.
* Product discovery now retains the exact inventory-source route for managed Storefront products, so buyers can stage own-inventory campaigns without a route-identity error.
* Creating a seller account is now more focused: company domain, billing currency, and support channel move to the guided setup steps after the account is created, where their purpose is clearer. Legal authorization is still required at account creation.
* Unenrolled accounts are unaffected and continue to see the current dialog.
## 5.358.0 — September 4, 2026 at 6:58 AM UTC
* Keep seller setup navigation legible and actionable by preventing workspace-tab overlap, naming incomplete inventory sources in the next step, and showing completed go-live steps without a strike-through.
* Keep seller workspace navigation in one rail, including live storefront and Agents-route follow-ups.
## 5.357.0 — September 4, 2026 at 4:18 AM UTC
* Sellers can request an interactive placement preview during Creative review for eligible third-party or unknown creative. No action is needed unless you choose to request or open a preview. This staged feature is included: it adds no package, entitlement, usage charge, or other billable surface.
* Buyers: requesting a preview sends your exact creative to an authorized preview provider only when a seller requests that preview so the seller can review it. Buyers have no additional action to take, and the provider does not receive your creative for static inspection, approval, or rejection alone.
* After the provider returns an expiring preview, sellers can choose to open it outside Interchange. Static source inspection remains inert, and sellers can approve or reject creative without requesting a preview.
* We will measure staged acceptance and adoption through the fixed lifecycle events `mcp_creative_preview.request`, `.returned_link`, `.external_open`, and `.expired_link`: request-to-returned-link completion rate, returned-link-to-open rate, and expired-link rate. These events contain no creative, URL, provider, or buyer identifiers.
## 5.356.2 — September 4, 2026 at 3:15 AM UTC
* The `test-sales-agent` validation skill's advertiser-creation step now retries (up to 3 attempts) instead of failing outright on a rare, transient brand-lookup timeout, so self-validating your sales agent through this skill is less likely to fail for reasons unrelated to your own implementation. Published as `test-sales-agent@1.5.1`.
## 5.356.0 — September 4, 2026 at 1:23 AM UTC
* TikTok Traffic media buys now create successfully even when TikTok automatically extends delivery to its search results page — nothing to change on your side. TikTok's platform doesn't let advertisers opt out of this for some objectives, so your ad may serve there in addition to your selected placement.
* Media Companies no longer have to approve their own managed campaigns twice. Uploading Creative in an exact own-supply campaign satisfies the Storefront creative review; confirming Campaign launch and requesting a non-cancellation update satisfy the Storefront media-buy approval. Cancellation keeps its separate approval policy. Manual approval settings continue to apply to external and sponsored buyers, while source-native creative approval, readiness, financial, and safety checks remain unchanged.
* The new **Buyer agents** page shows which automations can call Interchange as your organization, and what proves it. It lists your organization's own operator identity — with its principal ID and lifecycle state, but no credentials to manage — alongside every buyer agent you administer, each with its Interchange-issued principal ID, lifecycle state, and how many credentials are active. Identity is not authority: holding a principal or a credential grants no advertiser, account, spend, payment, or Terms access.
* From that page an organization administrator can issue a new identity-only credential, register an API key or OAuth client the organization already owns, rotate an API key, or revoke a credential. A secret is shown exactly once, in the response that creates or rotates it, and can't be retrieved again afterward — not by the console, the API, or support.
* Rotating or revoking a credential asks you to retype the buyer agent's name first. Revoking also states up front whether you're removing the buyer agent's last active credential — doing so suspends it until you add another — and revocation can't be undone: the same credential can never be registered again.
* The page is backed by a new read, `GET /api/v2/buyer-agent-principals`. It returns your operator principal and your buyer agents, each credential's type, active-or-retired state, timestamps, and the one identifier the rotate and revoke calls accept. It never returns a secret, and it carries no issuer URLs, organization IDs, domains, JWKS URLs, key IDs, or key fingerprints — those describe a credential, never the principal.
* Reads and changes both require you to be signed in directly as an administrator of the organization. Support, impersonated, simulated, and delegated sessions, API keys, and OAuth clients are all refused.
* See [Buyer-agent credentials](https://docs.interchange.io/v2/setup/buyer-agent-credentials).
* Seller names, media-buy details, and creative formats now render in text content for AI agents using the v3 buyer surface.
* Agents on hosts that only pass tool text to the model (Claude web among them) were seeing sellers as bare numeric ids in proposal, media-buy, and `request_proposals` results, and `get(kind: "media_buy")` ignored `include`. Every buyer read now names the seller beside its id, `get(kind: "media_buy")` reports the campaign, seller, gross budget, and flight, and honours `include: ["products"]` (line items with their packages and human-readable format labels), `["creatives"]`, and `["deliverySummary"]`. The single-buy REST read (`GET /media-buys/:mediaBuyId`) now carries `campaignId`. Nothing changes for callers that already read `structuredContent`; the same facts are simply mirrored into text.
* The Sellers page — where you choose which sellers to work with, set billing policy, and manage seller connections — is now available to every buyer account, in Claude, ChatGPT, and the Interchange web app. Seller connection management has moved there from the Settings → Connections pane, which still handles creative, data and measurement, and cloud-drive provider connections.
* Use one left-rail switch to move between Inventory, Advertisers, and Agents. Setup progress stays on Inventory, and each advertiser contains its campaigns.
* Setup checklist counts now show only the steps you still need to do; defaults and optional items no longer count as progress.
## 5.355.0 — September 3, 2026 at 5:31 PM UTC
* Buyers on the v3 agent surface can now open Advertisers, Campaigns, and a draft campaign's Review & go live receipt from inside the conversation — nothing to set up. Ask your agent "show my advertisers" or "show my campaigns" and it opens the live, interactive Page in any host that renders MCP Apps — Murph, Claude, or ChatGPT — instead of reciting a list. Ask whether a draft campaign is ready to launch (for example, "is the spring launch ready to go live?") and it opens Review & go live: the plan, the staged media buys and their budget split, why each buy is not live yet, and the readiness blockers still standing between the draft and launch. Reviewing never launches anything; going live remains the explicit, confirmed step it is today.
* Storefront readiness now reads the execution currency your ad server actually holds instead of inferring one from your storefront settings.
* Two things change for managed ad-server storefronts:
* A storefront that settles in more than one currency is no longer blocked because its ad server executes in a payment currency rather than the storefront default. Your ad server's own default is what has to be enabled for new buys and settleable.
* A source whose ad server has no execution currency is now reported as blocked, with the reason, instead of appearing healthy in a currency copied from your saved connection settings. Previously a FreeWheel source could show a green execution currency and still fail a buy at booking time.
* Editing an ad-server connection also keeps the execution currency the ad server already uses, rather than resetting it to your storefront default.
* Nothing to change on your side. If a storefront was blocked on this, it clears on its own. If a source now reports blocked, the reason names the exact failed check — follow **Ad-server execution currency is not ready** in the source troubleshooting guide.
* TikTok Traffic media buys that previously failed to create due to a placement-setting mismatch now succeed in more cases — nothing to change on your side. TikTok's platform doesn't always honor the requested placement setting on the first attempt; when it still doesn't, the buy fails safely instead of partially completing, the same as before.
* You can re-test an ad-server connection yourself again. Sync & diagnostics has a **Test connection** action that probes your ad server and shows what came back — pass or fail, with the ad server's own message and what to do about it. Nothing is required of you: if a FreeWheel source was refusing buys because its currency had never been observed, re-testing it now is what clears that.
* Every probe is also recorded, including one that never reaches your ad server. "Last probed" says when anyone last checked, and says **Never** when nobody has — which used to look identical to a check that ran months ago.
* For sellers reading this over the API: a `get` on an inventory source now returns `adServerProbe`, so a read reports when the connection was last probed instead of letting stored state pass for a live test result. `get` also accepts an ad-server connection id where it previously took only the source id.
* In the Buyers directory's account picker, you can now choose "Use Interchange default" to map a buyer relationship to your ad server's shared default advertiser instead of mapping a dedicated account for that buyer. It's offered when every active grant on the relationship settles through agent (Interchange-cleared) billing and the source's configured default advertiser currently reads back healthy.
* Interchange keeps the mapping healthy on your behalf: if the source's configured default advertiser later changes or stops reading back, the mapping is marked stale and execution pauses on it until it's reconciled or you choose a dedicated account instead — it never keeps executing against a default that's drifted. Relationships billed directly to the operator or advertiser still need a dedicated account mapping; this option isn't offered for them.
* Buyer-facing surfaces are unaffected — buyers still see one account per relationship, with no visibility into which option you chose.
## 5.354.0 — September 3, 2026 at 2:26 PM UTC
* Plan & Billing now shows organization setup in the correct order: accept terms, select a plan when eligible, add billing details, then add a payment method. Completed steps stay marked complete if an earlier prerequisite reopens.
* Plan & Billing now separates pending IU offers from accepted plans. Pending offers appear in a dedicated **IU offer** card with their next action, while **Your current plan** and **Your current terms** only show accepted or effective information. Existing prices and agreements are unchanged.
* A new **Connected apps** page in your account menu shows every AI app you've authorized to access your Interchange account — such as Claude or ChatGPT — including when it was first connected and when it last refreshed. You can disconnect any of them from the same page.
* VAST tags registered through the API have never been able to reach a managed ad server without a declared version — now you can supply one and fix that. If that's you, start setting `vast_version` on your VAST assets. If you don't register VAST tags, there is nothing to do: hosted video and every other creative type are unchanged.
* Set `vast_version` on a VAST asset — `2.0`, `3.0`, `4.0`, `4.1`, `4.2`, or `4.3` — when you create or update a creative:
* `json { "url_asset": { "url": "https://ads.example.com/tag.xml", "url_type": "vast", "vast_version": "2.0" } } `
* A VAST document is fetched at request time, not at registration, so we can't detect its version automatically — you have to declare it. Until now a tag registered through the API carried no version at all, so it could never satisfy the check a managed ad-server destination makes before accepting a tag.
* Two sync outcomes are worth telling apart:
* **`destination_profile_rejected`, reason `missing:vastVersion`** — the tag declared no version. Update the creative with `vast_version` and sync it again to get it delivered.
* **`destination_profile_not_certified`** — the version is outside what we hold delivery evidence for on that destination. That boundary reflects what we have proven, not what the ad server refuses, so there is nothing to correct in your creative. Certifying that route is Scope3's job: we have to establish delivery evidence on the destination before the version becomes eligible, and retrying the sync won't change the result until that happens. There's no self-serve status page for it today — ask Murph where the route stands, or raise it with your Scope3 contact.
* Declaring a version on a non-VAST URL asset is rejected as a contradiction rather than silently ignored.
* Existing enterprise and small-business customers do not need to take any action: accepted IU plans and billing amounts are unchanged.
* Billing administrators and builder/power buyers can now use **Admin → Rate Cards** (or the Rate Card API) to set the monthly subscription and included-IU allowance independently for a future offer. For example, a plan can charge a \$1,000 monthly subscription with IU usage billed separately, or include 250 IUs with no monthly subscription. After a customer selects the plan, **Plan & Billing** shows the subscription, included allowance, and IU usage price as separate terms. For novice buyers, the existing plan-selection flow is unchanged; the clearer labels appear automatically and require no action.
* Organization administrators can now open Plan & Billing directly from organization settings without returning to the main account screen first.
* Child-only Seller Account administrators who follow a Plan & Billing link now see that billing is managed by the parent organization instead of being silently sent back to account home.
* Publisher authorization checks now agree with AAO when an authoritative manager file uses `publisher_properties`, repeats an agent URL for separate scopes, or mixes `www` and apex domains. When you connect or update a third-party sales agent, Interchange also rejects an `adagents.json` document as the agent endpoint and asks for the executable URL from `authorized_agents[].url` instead.
* Keep repeated unavailable Buyers actions visible and open Business Rules during storefront setup before approval capabilities are enabled.
* TikTok Reach media buys now pass readback verification and create successfully. Every TikTok Reach ad group was previously rejected after creation because it requested an optimization location TikTok doesn't use for Reach campaigns; the ad group now requests the optimization location TikTok's Reach objective actually expects.
* Wholesale pricing uploads now commit for every ad server's own selector types. If you price FreeWheel site groups or site sections, SpringServe supply tags, or AdsWizz zones, a clean preview was followed by a commit failure naming an unsupported selector type — even for the selector ids our own pre-filled template gave you. Those feeds now commit, and your selector type is stored exactly as you sent it, so the pricing lands on the products keyed to it.
* Nothing about your file needs to change: upload the template as-is. A `selector_type` that matches none of your products still previews as an unmatched selector and prices nothing — that warning is the signal to check the id and type against the selector browser, and it no longer blocks the rest of the feed.
* When your storefront supports composed products, seller home now starts with “Interview your Merchandising Agent” and gives you two direct actions: teach it how your business sells or test it with a real buyer brief. Storefronts without composition capability keep the existing neutral home experience. This changes only the seller home entry point; it does not change what buyers see about your storefront.
* The explanation for an unavailable **Fund your account** action on Plan & Billing now remains fully visible instead of being cut off at the edge of the **Standing & funding** card. Nothing changes on your side.
* In the Seller Dashboard's action row for a storefront that sells through its own sales agent: when merchandising is on your plan and a connected sales agent can supply products but hasn't finished setup, you now see "Review source readiness," which opens that source's diagnostics, instead of "Connect an ad server." When no connected source can supply products yet, you still see "Connect an ad server." When merchandising isn't on your plan, you still see "Upgrade to a merchandising plan."
* "Listing-only account" now replaces "pass-through" on the plan screens and the Seller Dashboard badge — in renewal and non-renewal status lines, the free-plan confirmation step, and the badge label itself. The merchandising source prerequisite is stated in plain language everywhere it appears: a connected source that is ready to supply products your storefront can sell, such as an ad server.
* Buyer-facing surfaces are unaffected — every changed string is on the Seller Dashboard, the plan screens, or the seller glossary/onboarding docs.
* Source Diagnostics now reports only the discovery calls your diagnostics actually recorded. When a validation test ended without returning a usable result, the Product discovery row previously added a call of its own and showed "Calls 1 / Failed 1", which read as a Source fault even when no such call had been recorded. The row now leaves the recorded counts untouched and says no test result was returned, so a test that gave you no answer is no longer indistinguishable from a Source that answered badly. Whether the Source was reached in that case is genuinely unknown until the diagnostics refresh, and the Page now says so instead of guessing. Run-specific request, response, and trace controls still appear only when the returned result includes their identifiers.
* Your seller rail stays in place when you open setup widgets, and newly created accounts open directly. Nothing to change on your side.
* For managed ad servers, connection responses now include the linked inventory-source name. Existing integrations continue to work without changes.
## 5.353.0 — September 3, 2026 at 7:17 AM UTC
* The Buyers account picker offers **Use Interchange default** for buyers you bill through Interchange, once it's turned on for your storefront and your connected source has a healthy configured default account. One click settles that buyer through your existing default instead of creating or picking a dedicated native account — no setup required. The option only appears when it's actually usable; it stays hidden for direct-billed buyers, a source without a configured default, or a storefront where the exit hasn't been enabled yet.
## 5.352.0 — September 3, 2026 at 6:15 AM UTC
* Creative creation now commits its manifest, assets, metadata, and completion marker together. Failed saves leave no partial creative records, and a concurrent same-name request removes only its own prepared object batch before returning the existing creative; existing routes and V2 ordering remain unchanged.
* Buyers using their own connected Storefront can now reliably continue selecting products during campaign setup. You do not need to take any action. Sellers and their Storefront settings are unaffected.
* **Scrappy builder / power buyer:** On a configured Meta connection, you can use the MCP upload Task to add one private JPEG or PNG for the ad account you selected. If image upload is not shown, use a configured Meta connection; do not send the file through chat. Managed MP4 and audio uploads keep their existing account-access and service settings.
* **Enterprise brand buyer:** No action or governance change.
* **Large agency / hold-co buyer:** Existing team workflows do not change.
* **SMB buyer (novice, chat):** Nothing changes in chat.
* **Seller check:** This does not change seller ranking, inventory presentation, readiness, or setup.
* Fixed several issues on the storefront Listing page's ChatGPT app and Distribution blocks:
* The generated-icon preview no longer shows a broken-image icon if it can't load — it falls back to a plain tile instead.
* Directory and composer logo uploads now use an "Upload PNG" button instead of the browser's native file picker, with the chosen file's name shown next to it. Choosing the same file again (for example after replacing a logo and wanting to revert) now works correctly.
* The ChatGPT app's category is now tucked behind a collapsed "Advanced — change category" section, since it's already defaulted for you and most sellers never need to change it — but the current category always shows as a read-only row above, so you can see and confirm it before saving. If you change the category without saving, "Mark listing reviewed" is disabled until you save, so you're never reviewing one category while approving another.
* Approving your ChatGPT app listing ("Mark listing reviewed") now also covers the category shown at that time, the same way it already covers your listing name, subtitle, description, and URLs. **If you approved your listing before this change and have since changed the category, you'll be asked to review and approve it again** — a one-time reset closing a gap where a category change alone didn't require re-review.
* OpenAI Apps verification now appears only when Public web is selected, since that's the only time there's a public discovery URL for it to verify.
* Meta Feed Image, Feed Video, Stories, and Reels no longer advertise Call Now as a generic creative CTA. This change does not add a new Call Now option; use the existing Meta Calls product workflow when that product is available and configured.
* Seller impact: not applicable. This changes buyer capability discovery only; it does not change Storefront seller setup or delivery.
* Asking about a modular inventory source now returns its complete module list. Reading modular readiness returns tens of kilobytes of module contracts and per-stage field requirements, and an assistant that could only hold part of that answered from the part it had — reporting a source as having fewer modules than it does, and in one case describing an active trafficking module as absent. Modular source reads now state the module roster and the lifecycle stages it covers up front, so the list is complete regardless of how much of the detail behind it fits, and a partial read now says so instead of reading as a short one.
* Text fields (brand name, headline, etc.) now appear in the Upload tab when creating a creative for social platforms such as Snap, TikTok, Meta, Reddit, Pinterest, Spotify, and LinkedIn. Previously these fields were missing from the upload form, causing creatives to be rejected by the platform as invalid.
* Fixed a bug where creating a TikTok media buy for a non-conversion-tracking objective (e.g. Traffic) always failed with a false targeting mismatch, even though nothing was actually wrong with the campaign.
* TikTok Reach media buys now create successfully. Every TikTok Reach ad group was previously rejected by TikTok as invalid, because the ad group's frequency settings were only sent when a frequency cap was explicitly requested. TikTok requires a frequency setting for Reach campaigns, so one is now always included — using your requested frequency cap when you set one, or a default sized to your flight dates otherwise.
* The Sellers page in Claude no longer shows a blank surface while it connects and retries automatically if the first connection stalls. Also registers record\_widget\_telemetry, an app-only, model-invisible observability tool with a closed fixed-code schema; it adds no customer capability, so this stays a fix.
* Technical buyer administrators can now issue a new identity-only credential for a buyer automation, instead of only registering one the organization already owns. Like a registered credential, it authenticates the automation only — no access to advertisers, accounts, spend, payments, Terms, or other business APIs.
* A directly authenticated organization administrator issues it through the buyer REST API, and can replace it later without changing the automation's identity. Save the one-time client secret when it is returned — it cannot be shown again. If an issuance comes back needing reconciliation, retry the same request with the same idempotency key before trying anything else. There is nothing new for chat-only buyers or sellers to configure.
* The Seller Dashboard for accounts that sell through their own sales agent no longer shows a "Turn on merchandising" button that granted nothing when clicked. It now shows the one control that matches your account: sellers without a merchandising plan see "Upgrade to a merchandising plan" and go straight to Plan & Billing; sellers who already have a plan but no connected ad server see "Connect an ad server" instead. Sellers who already have both see no control here — merchandising is already on.
* Embedded sales agent 1.42.0. GAM-backed storefronts can now target every country GAM itself can target, including region-typed country targets, and a request naming a country the agent cannot map now fails closed instead of being silently narrowed. FreeWheel-backed storefronts are buyable again, and a media buy binds to the FreeWheel account the request names rather than a default.
* Corrected the `update_storefront_capabilities` documentation to match actual behavior: recording `setupIntent` (`third_party_connect` | `sell_through_scope3`) is purely descriptive and has never applied a capability preset or been rejected on ad-server-backed storefronts — `capabilities` (derived from merchandising entitlement and Source product readiness) is the only thing that changes what a storefront can do. Removed the dead, uncalled compatibility helper that previously implemented the (already inert) preset.
* Also documented an existing validation rule that already applies today, with the full rule and worked examples in Storefront onboarding → *Approval-routing prerequisite*. On an existing storefront that is not routed through an adapter, a save is rejected with a `400` (field-scoped to `mediaBuyApproval` or `creativeApproval`) when it results in that approval setting being `manual` **and** the matching capability (`offersCampaignApproval` or `offersCreativeReview`) being effectively `true` — unless that exact pair already held immediately before the save, or approvals of that kind already have somewhere to route: an active primary approval-routing policy for it, or, when none is saved, at least one active organization admin (the default). Routing is opt-out, so most sellers need no setup; configure an approval-routing policy only if approvals should go somewhere other than your organization admins. No behavior changed here — this makes a rule that was already enforced visible.
* Shareable buyer invite links have been retired. Opening a saved or shareable buyer invite link now shows a not-found (404) page and does not start signup. To access a seller, open the seller’s verified storefront or find the seller through Discovery. If you do not use saved or shared buyer links, no action is required.
* Sellers: replace any shared buyer-link directions with your verified storefront or Discovery page. Your existing buyer approval settings are unchanged.
* Enterprise and agency buyers: update saved links and team playbooks to use sellers’ verified storefronts or Discovery. There is no replacement invite-link workflow.
* SMB buyers: if a link no longer opens, search for the seller in Discovery or open the seller’s verified storefront. No technical setup is required.
* API and MCP builders: remove calls that create, list, delete, or resolve buyer links, and remove `joinSlug` from requests. Authenticated calls to retired APIs are not found (404); anonymous requests to the formerly public resolver are rejected by authentication without disclosing context. `joinSlug` is rejected as invalid input. Send users to the seller’s verified storefront or Discovery instead. There is no replacement targeted-invite API.
* Packaging and pricing are unchanged.
* Sellers can now declare the full AdCP MediaChannel vocabulary on their listing — including search, linear TV, radio, podcast, out-of-home, print, cinema, email, gaming, retail media, influencer, affiliate, product placement, and sponsored intelligence — instead of the previous six-channel set. Saving still accepts the legacy `audio`/`video` spellings for backward compatibility; `video` is now saved as `olv` and `audio` keeps its established spelling, so nothing already using either alias needs to change.
## 5.351.0 — September 2, 2026 at 10:35 PM UTC
* `save_media_buy` now stages auction-priced products and tells you plainly when a request cannot be applied.
* **Auction products can be staged.** Pass `bidPrice` (in the campaign currency) on any `products[]` entry whose pricing option is auction-based. Fixed-price products are unchanged; omit `bidPrice` for them.
* **Budget changes on an existing draft no longer silently no-op.** Calling `save_media_buy` with `mediaBuyId` and a `budget` now returns a `NOT_IMPLEMENTED` error explaining that the budget set at staging time is preserved, instead of returning success without applying the change.
* **Same-day and past start times are rejected up front.** On the update path, a `flight.startAt` that falls today (UTC) or earlier returns a `VALIDATION_ERROR` on `flight.startAt` with guidance to pass a later date-time or `"asap"`. Media buys start on a future UTC day, so this replaces the previous behavior of accepting the timestamp and then either failing at the seller or quietly starting as soon as possible.
* Nothing changes for agents that only stage fixed-price products and never send a `budget` or past `flight.startAt` on update.
* **If your agent calls `get_proposals_diagnostic` directly, you must switch to `request_proposals` with the same `idempotencyKey` to poll a running execution.** Agents that do not call `get_proposals_diagnostic` only see the `request_proposals` error-message change described below.
* `get_proposals_diagnostic` has been removed from the v3 buyer MCP surface. The `request_proposals` tool already supports idempotent polling: calling it again with the same `idempotencyKey` returns the in-progress or completed result without starting a new execution. That pattern is now the only supported way to poll.
* Agents that currently call `get_proposals_diagnostic` will receive an `unknown-tool` error after this change and must migrate to the `request_proposals` polling pattern before then.
* **Also in `request_proposals`:** when a seller's agent reports an error, the per-seller `error.message` now carries a short Scope3-written explanation (for example, "The seller returned PRODUCT\_NOT\_FOUND. Seller-supplied error text is withheld from this response.") instead of the seller's own free text, and the `code` is limited to standard AdCP codes with `SELLER_ERROR` as the fallback. Errors raised by Scope3 itself (for example a product snapshot that could not be saved, or no connected sales agent for the seller) keep their original code and message. If your agent displayed a seller's raw message, expect the neutral text in its place.
* In Plan & Billing, you will now see your accepted, priced activity in IUs and a breakdown that matches your IU balance. Scope3's provider costs are no longer shown as your IU activity, and no action is needed.
## 5.350.3 — September 2, 2026 at 7:31 PM UTC
* Buyers using your own inventory: We fixed an issue that could lose the selected product during activation. This selection fix applies only to own-inventory campaigns; third-party inventory selection is unchanged.
* Buyers with saved creative links: Supported seller destination URL slots now receive the saved clickthrough link instead of dropping it from the seller request.
* Sellers: This restores the buyer-provided destination in supported URL slots. It does not change how your inventory is listed or which buyer campaigns you can see.
* Existing saved campaigns and creatives use the corrected behavior on their next activation; they do not need to be recreated. No setup changes are required.
## 5.350.2 — September 2, 2026 at 3:30 PM UTC
* Own-supply product discovery now preserves a completed catalog when a seller responds during the first progressive window, so immediate continuation calls return the same selectable inventory instead of an empty result. Nothing needs to change on your side.
## 5.350.1 — September 2, 2026 at 12:22 PM UTC
* The Company tab's setup checklist no longer shows a completed "Company identity" step once your company is admitted. The country picker in Branding & distribution groups popular countries first, and its "Add country" control no longer claims to be a search field. The printable-card button explains why it's disabled whenever the current listing isn't published, not just in one narrow case. The ChatGPT app icon uploads now state the exact size and file-size limit required, and can reuse your storefront's own generated icon instead of a manual upload.
* Product discovery continuations now preserve the completed catalog from the original request instead of unexpectedly returning different or missing inventory.
* Product discovery now keeps `supported_targeting` stable across provisional and terminal catalog reads, so an undeclared targeting list cannot invalidate an own-supply product selection.
## 5.350.0 — September 2, 2026 at 10:47 AM UTC
* Preserve the host-neutral Company identity task destination and cover real iframe source-chooser actions in the setup-nav browser regression gate.
* The Company identity & eligibility setup row now displays its label in the seller rail.
## 5.349.0 — September 2, 2026 at 9:15 AM UTC
* Changing geo targeting on an existing Meta media buy through `update_media_buy` no longer fails. A package's budget, pacing, and bid price on a Meta media buy now actually reach Meta when you change them — previously these appeared to succeed but were silently never applied on Meta's side. If you changed a package's budget, pacing, or bid price on a Meta media buy before this fix, resend that update to make sure it took effect. Changing an individual package's flight dates isn't supported on Meta; use the media buy's overall start and end date instead.
* Products returned by `get_products` now include a `supported_targeting` field listing the targeting dimensions the product accepts as an overlay (e.g. `"geo_countries"`, `"language"`, `"device_type"`). Use this to avoid forwarding overlay dimensions a product cannot resolve — absence means the seller did not declare targeting support.
## 5.347.0 — September 2, 2026 at 6:30 AM UTC
* Sellers can now list VAST-delivered audio, seller-rendered stateful display, and coordinated multi-placement products, and buyers can select them without those formats being rejected or dropped. Sellers who want to offer them must add the matching `audio_vast`, `seller_rendered_stateful_display`, or `coordinated_placements` declaration to their products. Existing products, including DAAST and hosted-audio formats, are unchanged.
* VAST audio now stays intact from product discovery through selection, validation, pricing, and creative delivery. Collection-based targeting from AdCP 3.2 is also preserved in requests instead of being discarded. Buyers who only use existing formats do not need to take any action.
* **Who this is for:** Enterprise brand buyers gain access to additional seller inventory without a governance, partner-instruction, measurement, or billing change. Large agencies can compare and select the new formats across sellers without changing playbooks for existing formats. Scrappy builders can use the new `format_kind` values directly and can pass collection selections without them being dropped. SMB buyers using chat do not need to configure anything; the product adopts compatible seller capabilities automatically. Sellers that opt in should add the matching declarations and monitor catalog completeness and creative-readiness results for those products.
* **Packaging and monetization:** There is no entitlement, plan, price, billable-surface, unit-economics, commercial-package, or Interchange Unit change. These formats and collection fields are additive capabilities inside the existing buyer and seller surfaces.
* **Availability and rollback:** This ships as one ungated contract update because a partial flag would let discovery, selection, and validation disagree about the same format. Exposure is still opt-in at the protocol boundary: a seller must declare a new format, and a buyer must select it or send collection selection fields. Existing formats remain the default. Broader promotion of each new format requires at least one seller discovery and one successful buyer selection for that format, with no confirmed schema mismatch. The kill switch is an atomic code revert that removes the new SDK/schema version and derived format declarations together; 3.2-only requests then fail closed while existing hosted-audio, DAAST, display, and placement paths continue unchanged.
* **Measurement:** Track `adcp_3_2_format_discovered` as the first successful product response per seller and buyer customer containing each new `format_kind`; track `adcp_3_2_format_selected` for successful media-buy or creative submissions using those kinds; and track `adcp_3_2_collection_selection_preserved` when a request with collection selection reaches the downstream canonical request intact. During the first 30 days, establish unique-seller and unique-buyer baselines for each signal, require zero confirmed audio/video VAST misclassifications and zero dropped collection selections, and revert on a reproducible contract mismatch. Brian O'Kelley owns the 30-day review through AI-6639; low adoption creates a discoverability follow-up rather than weakening the contract checks.
* Technical buyer administrators can now connect an existing organization API key or WorkOS machine credential to a distinct buyer-agent identity, and turn off that identity from the same API. This helps hands-on teams keep an agent's authentication separate from a person's account without granting it access to advertisers, accounts, spending, payments, or Terms.
* Existing integrations do not change. To use this capability, a directly authenticated organization administrator registers a credential the organization already owns. There is nothing new for chat-only buyers or sellers to configure. Creating a new credential or swapping one credential for another is not included.
* Connecting a seller no longer dead-ends silently when an authorization link is opened more than once: the link pages now explain what happened (recently used or expired) and how long links stay valid (10 minutes). Bearer (access-token) submissions remain strictly single-use.
* Agent-visible text for seller and connection reads now includes discovered ad-account names and mapping states (not just a count), says explicitly when an advertiser transacts on account-level seller defaults, shows seller names instead of "Customer NNNN" placeholders, and prints names beside ids.
* We renamed the seller identity/story object buyers see in Marketplace — description, channels, accepted countries — from "media kit" / "Discovery Card" to **Listing**, and updated every agent-facing tool, error message, and doc to match. The v3 `get` seller include for it is now spelled `listing` — request it with `include: ["listing"]`. The deprecated `discoveryCard` spelling keeps working as an alias with no removal timeline: the response echoes back whichever spelling you sent, so an existing integration reading `object.discoveryCard` is not broken by the rename. All other agent- and buyer-facing tool descriptions, error messages, and documentation now consistently use listing / distribution / brand domain vocabulary in place of the retired media kit and Discovery Card names; "operator domain" is retired from seller-facing copy in favor of brand domain (it remains the name for a buying operator's own domain on the buyer side, e.g. house discounts).
* This also fixes a real routing bug in the v3 `open_page` navigation tool, which had fallen out of sync with the rename above: `page: "listing"` had been opening the legacy Business Profile merchandising page instead of the current Listing page it's named for. Fixed now, with three concrete behavior changes for any agent or integration calling `open_page` directly:
* **`page: "listing"`** now opens the Branding & distribution page (the current Listing) — it briefly opened the legacy Business Profile page instead; that misrouting is what this release corrects.
* **`page: "media_kit"`** (a deprecated spelling of `listing`) previously opened that same legacy Business Profile page. It no longer opens anything: the call now returns an explicit, self-describing error naming both successors, so any caller using this key must switch to `page: "listing"` (Branding & distribution) or `page: "business_profile"` (the legacy page, still fully reachable) — the ambiguity between the two meanings this one spelling had accumulated over time made silently picking either wrong for whoever meant the other.
* **`page: "discovery_card"`** (a deprecated spelling that always meant the current Listing page, never the legacy one) keeps working unchanged, now with an on-response note pointing callers at the canonical `listing` spelling.
* This ships as `improvement`, not `feature`, because the affected key (`media_kit`) is a deprecated alias on the v3 seller surface — a young, admin-gated integration path, not the primary customer-facing surface — and the new error is self-describing enough that an agent caller reads it and corrects its next call in the same turn, rather than needing new documentation or a workflow change. The repointing itself was ratified by the repo owner as a bug correction, not a new capability: nothing a seller or buyer does in the product changes.
* **Sellers using the seller UI:** no action needed. The UI does not call `open_page` with `page: "media_kit"`, and your listing, distribution settings, and pages are otherwise untouched — only names changed, and every other deprecated spelling keeps working. **Developers or agents calling `open_page({page: "media_kit"})` directly** should switch that one call to `page: "listing"` or `page: "business_profile"`, per above.
* `execute_campaign` on Spotify, Snap, and TikTok adapter storefronts no longer fails with "ESA create dispatch returned an invalid structured response". These adapters now return a `media_buy_status` of `pending_creatives` after a successful create (campaign paused, awaiting creative assignment), which is the correct lifecycle state and allows the host to record the submission.
* Fixed the storefront's `advertisedCountries` and `advertisedChannels` to only reflect Sources whose product path currently routes through your own external sales agent. A Source that instead supplies Storefront-built (wholesale) products no longer contributes its raw upstream declaration to this coverage — that path's evidence comes from its synced inventory instead. Storefronts with a mix of agent-supplied and Storefront-built Sources, or a Source pending a required mode selection, may see these fields narrow to the coverage their external agent actually advertises.
* The same fix now also applies to market membership and public market readiness: a Storefront-built (wholesale-only) Source can no longer make a storefront falsely appear tagged into a market, or make the public market readiness preview report supply that isn't actually agent-supplied.
* Snap media buys with country, region, or postal targeting now create and update correctly. Two bugs were fixed: ad squad write requests were missing the `targeting_v2=ENABLED` query parameter that Snap requires to parse geo targeting, causing all geo-targeted creates to be silently rejected; and the postal-area validation guard did not recognise the `{country, system: "us_zip"}` shape that Interchange's schema produces, causing US ZIP targeting to be rejected before reaching Snap.
* Spotify media buys with a package budget below Spotify's $250 per-ad-set minimum now fail immediately with a correctable error message ("Spotify requires a minimum budget of $250 per ad set") before any campaign is created, instead of silently inflating the budget to \$250 with no signal to the buyer.
* If sign-in recognizes your verified email but account setup was interrupted, Interchange now returns you to signup with your details prefilled so you can finish. This works for buyer and seller accounts; no action is needed unless you encounter this interrupted state, and you do not need to reset your password.
## 5.346.1 — September 2, 2026 at 3:03 AM UTC
* Agent operators can now open and complete Configure connection from the Agent page, with a visible error instead of a silent click if the task cannot open.
## 5.346.0 — September 2, 2026 at 12:54 AM UTC
* Partner operators can now refer a publisher from an Agent's Sources section. The client workspace opens on that exact Agent, preselects it in the invitation, and keeps the Agent's pending referrals in view.
## 5.345.0 — September 1, 2026 at 10:35 PM UTC
* Open Plan & Billing help links on the current Interchange documentation site.
* Show “Not priced yet” instead of “NaN IUs” when an activity rate is unavailable.
* Seller account tiers now have clearer names throughout sign-up, plan copy, and documentation: a **Listing-only account** gets your listing on Interchange while your own sales agent handles selling, and a **Merchandising account** adds full merchandising functionality. The old "pass-through" plan wording is retired from sign-up, plan copy, and docs.
* New Media Company signups now land on a short company-confirmation page before they reach account setup. This is a limited early-access rollout — existing accounts aren't affected and don't need to do anything.
* Right after you create an account, we show the company name and domain we found from your sign-up email and ask you to confirm or correct them, along with the billing country of the contracting company (not your personal location). That country is the only input to your plan currency: there's no currency picker, and a country we don't support yet shows next steps instead of defaulting to USD. Confirming also records your acceptance of the Terms of Service and disclosure of the Privacy Policy.
* Confirming starts a short admission check, usually a few seconds. If it runs longer, you can close the page — refreshing or logging back in picks up the same check instead of starting over. Once approved, your first setup screen already shows "Company identity" as done on the setup checklist.
* Label the account-standing payment arrangement as “Billing method.”
* Media-buy status updates now complete reliably when multiple sellers respond at the same time. No action is required on your side.
## 5.344.0 — September 1, 2026 at 8:36 PM UTC
* Demo Storefront accounts now provision their synthetic inventory and setup checklist when created from the account switcher.
* Keep scoped Murph workspaces focused on their own canvas, bring newly opened workspace widgets into view, and preserve advertiser scope IDs in shareable URLs.
* Keep enrolled sellers on their setup checklist while it loads or retries, without showing another account's cached rail.
* Your setup checklist rail and payout status page now reflect your actual account state. If a required payout or setup step is missing, you now see the action to take instead of a completed status. Sellers whose status was already accurate do not need to take any action; this seller-side correction does not change buyer-facing storefront capabilities.
* Prevent seller setup source actions from regressing into dead controls.
* The v3 `save_advertiser` and `save_campaign` tools now accept `gvlVendorId` on tracker inputs. Set this to your GAM TCF vendor ID to enable vendor-scoped consent macros in your ad tags. For sellers: trackers that carry a vendor-scoped macro will now include vendor-specific consent values in the outbound pixel — tags without a vendor ID are unchanged.
## 5.343.0 — September 1, 2026 at 6:00 PM UTC
* Asking Murph what you need to go live now opens your redesigned Buyer Setup page — the platform go-live checklist, your operator identity, and each seller or platform's remaining connection, mapping, or billing step — instead of a long text summary. Murph gives a one-line lead-in and lets you act in the page, and no longer answers a buyer go-live question with seller-side readiness detail. The page is rebuilt on the Scope3 design system.
* Fixed a bug where forwarding a media buy to a third-party AGENT storefront source that returns a synchronous acceptance would report "No source accepted the media buy" to the buyer. The source accepted the buy successfully; our own inline handler threw because it looked up a buyer-side media buy record that only exists on the buyer's tenant, not the storefront's.
* External-agent storefronts now recognize publisher authorization for their connected sales agents.
* If your storefront sells only through connected external AdCP agents, each publisher's `adagents.json` is checked against those agents' published URLs. The publisher is authorized when the file grants property authority to at least one connected agent, so publishers no longer show a false "adagents.json does not list your sales agent" warning when they authorize the agent that serves their inventory. Existing affected publishers are queued for a new authorization check, and later agent URL or source-status changes trigger another check automatically. Storefronts with operator-run inventory continue to use `https://interchange.io` as their authorization identity. Nothing to change on your side.
* When you change a package's targeting (geography, audience, or other overlay fields) with `update_media_buy`, the call now waits for your seller to confirm the change before reporting success. If the seller can't confirm it, you get an error instead of a false success. Nothing to change on your side.
* Sellers holding a global market-maker authority now qualify for advertisers in every market, even when the seller's storefront hasn't enumerated regions. Previously an empty region list silently disqualified every advertiser with a misleading market-mismatch message.
* Sellers with only regional authorities, and sellers with no authority at all, keep their existing region-list behavior.
* Playbook version history no longer displays internal bookkeeping notes.
* The v3 agent surface's deprecated compatibility singleton for the legacy Business Profile is now spelled `listing` on `get`/`open_page`, and `save_seller` accepts a grouped `listing` write field. `media_kit`/`mediaKit` keep working as deprecated aliases for the identical handler — no removal timeline is set. Sending both `listing` and the deprecated `mediaKit` alias with different values is rejected; identical values are accepted.
## 5.342.1 — September 1, 2026 at 1:51 PM UTC
* Sponsored sandbox buyers can now request proposals from their sponsoring storefront while it is still in sandbox readiness.
* Seller integrations can now match each material-candidate, RFP representation, and feedback response to its request more reliably. The service echoes the request, RFP, and turn identifiers you provide and includes generated representation or source-revision identifiers where applicable. When an existing representation is reused, `clientRequestId` identifies this call and `originatingClientRequestId` identifies the request behind the shared representation and its validation receipt. No request format or action is changing, and existing stored records continue to replay for older clients.
* Clarify directed campaign subscription instructions while keeping the V3 buyer tool surface within its context budget.
## 5.342.0 — September 1, 2026 at 12:50 PM UTC
* Creative reviewers can now request a placement-specific preview result when the seller's completed package response identifies one exact format and placement. Interchange preserves and labels publisher-authoritative and representative seller results, including fidelity and expiry, while keeping raw provider URLs, HTML, submitted third-party tags, and remote VAST inert in the browser. Reviews without an exact authority context continue to show the submitted source only.
* Stage a creative on an advertiser before you have a campaign. `save_creative` now accepts an `advertiserId` in place of `campaignId` and creates an advertiser-level master. The same call previously failed with `campaignId is required to create a creative`, so creative work had to wait for a campaign to exist. The master won't deliver until you assign it to a campaign, and it still needs both content (`message`, `assets`, or `clickUrl`) and a format — `formatKind`, unless an image, video, audio, HTML, or VAST asset implies it. Name and scope alone are rejected, and so is a brief with no format. Nothing changes for campaign-scoped creates.
* When `save_creative` persists fewer assets than you sent, it now tells you how many attached and sets `assetAttachWarning` on the response — a saved click-through URL can no longer make a dropped image look like success. Creating a creative on an advertiser also now confirms the advertiser belongs to your account before writing anything.
* The Listing page now leads with your storefront's listing content once, replaces the visibility dropdown and separate Publish button with a single "Where can buyers find you?" control (Hidden / Interchange / Public web), and moves the dedicated-hostname setup behind an "Advanced" disclosure so it only shows up when you're serving on your own domain. The ChatGPT app section is now a compact status row with a collapsed preview of what ChatGPT shows, instead of a full settings form.
* Nothing to change on your side — your current visibility, listing content, and ChatGPT app setup are unaffected. Only visit the page if you want to adjust who can find you or explore the streamlined layout.
* Buyers: Media buys now remain bound to the exact Storefront and inventory source selected in product discovery. No action is needed in the Interchange UI or Murph, or for API and MCP integrations that already store and return `productId` unchanged. An integration that modifies or rebuilds this opaque value must instead persist the value returned by discovery. If a previously stored value could select more than one inventory source, the request now stops instead of choosing one unpredictably; run discovery again and retry with the new `productId`. The field name and string type are unchanged.
* Sellers: No action is required. This does not change product setup or how a Storefront publishes inventory; it keeps the buyer's selection tied to the product route returned through discovery.
* In the updated Buyers view, a relationship held only on billing readiness (no open request left to review) now has a "Resolve billing" action that opens the billing-readiness section directly, so you can assert readiness again without a dead end.
* You can also withdraw your own pending information request without deciding the whole case — the case stays open and returns to your queue, instead of your only option being to decline it.
* The setup-nav checklist rail now appears immediately when you open Murph — no more flash of the old navigation before the checklist loads. A skeleton placeholder appears in its place during the brief load window, and the checklist renders as soon as your storefront status arrives. Your last-loaded checklist state is also persisted locally so subsequent visits render the checklist instantly while a fresh update loads in the background.
* Embedded sales agent 1.41.0: replacing an ad-server connection no longer loses the destination advertiser it was mapped to, FreeWheel advertiser rosters are no longer cut off at a fixed ceiling, and FreeWheel execution currency now bootstraps correctly instead of blocking first use. The agent also now publishes an advertiser-account capability descriptor (starting with Google Ad Manager) that tells the platform exactly which account operations each ad server supports — groundwork for smoother buyer account setup — and runs on the AdCP Python SDK 8 beta.
* The seller Setup page that shows your identity, description, accepted channels, and countries to buyers is now called your **listing** instead of "media kit." Sellers with a ChatGPT app still see it referred to as your "ChatGPT app listing," which is a read-only projection of the same listing. This also fixes a stretched status chip on the Branding & distribution page, removes a duplicated identity display, and rewrites the brand domain explainer in plain language.
* The seller detail drawer now puts a Reconnect action at the top when a connection needs re-authorization, explains market/channel mismatches by naming the mismatched markets instead of a generic line, and adds search and status filters to the advertiser list.
* Buyers on the v3 MCP surface can now discover marketplace-listed sellers they have not yet connected to via `search(kind:"seller", filter:{marketplace:true})`. Channel filters accept multiple values with OR semantics: a seller qualifies if any listed channel matches. Existing `search(kind:"seller")` calls without `marketplace:true` are unchanged and continue to return only your connected sellers - no migration needed.
* To subscribe to a connected seller's directed campaigns and mirror them into your reporting view, call `save_directed_campaign_subscription`. Subscription requires an existing seller connection. On partial failure, `errors` lists which subscriptions failed; successful subscriptions are not rolled back. Retry failed items individually.
* **For sellers:** marketplace-listed sellers become discoverable by all buyers using the new filter. Directed-campaign subscriptions only mirror campaigns the seller has explicitly listed; no undisclosed seller data is surfaced.
## 5.341.1 — September 1, 2026 at 8:01 AM UTC
* The Sellers page and other Murph-surface views no longer risk a render loop (React maximum-update-depth crash) when a navigation updates the page's URL parameters.
## 5.341.0 — September 1, 2026 at 6:13 AM UTC
* Disconnecting Interchange from Claude, ChatGPT, or any AI assistant now ends your access immediately — before, your connection stayed live for up to 30 days after you disconnected. No changes needed; any AI assistant connected to Interchange picks this up automatically.
* The Sellers page widget now sizes correctly when opened directly on a seller's detail drawer, instead of clipping the drawer to a short band.
## 5.340.0 — September 1, 2026 at 4:15 AM UTC
* MCP connections no longer drop when a host refreshes credentials from multiple components at once. A 90-second grace window lets concurrent legitimate clients (such as multiple Claude.ai components sharing one refresh token) receive the same successor pair instead of each one's reuse triggering family revocation.
* MCP clients that received a grant for `https://api.interchange.io/mcp` can now call `/mcp/v3`.
* Clients (e.g. Codex/openai-mcp) that completed the OAuth flow against `/mcp` before discovering the versioned endpoint were rejected with `resource_mismatch` on every `/mcp/v3` call. The server now accepts that specific grant at `/mcp/v3` only — no other endpoint is covered by this change. Grants issued for any other resource (including `/mcp/v2/admin`, `/mcp/v2/tars`, `/mcp/tars`, `/mcp/creative`, or any other child of `/mcp`) still require an exact-resource match. New grants issued from a `/mcp/v3` authorization flow continue to bind to the exact endpoint.
## 5.339.0 — September 1, 2026 at 3:11 AM UTC
* Fixed five issues in the seller setup checklist rail. Branding & distribution now opens your media kit instead of the property roster. Business rules opens in place instead of navigating away to an empty page. Buyers shows a clear "can't open this right now" message instead of silently opening the sales agent surface when self-serve buyers isn't enabled for your storefront. Plan & billing now renders with the same card styling as every other checklist item. And a Google Ad Manager sign-in problem no longer shows up as a warning on unrelated modular sources that have no GAM connection to fail.
## 5.338.1 — September 1, 2026 at 2:16 AM UTC
* Buyer Sellers now recovers on the next request after a temporary access-check failure, instead of remaining hidden for up to five minutes. Explicitly disabled access remains hidden, and customer and parent-account isolation are unchanged.
## 5.338.0 — August 31, 2026 at 11:27 PM UTC
* Fixed `execute_campaign`/`create_media_buy` and `get_media_buys` failing for Meta media buys because Meta's raw timestamp format was passed straight through into `confirmed_at`/`created_at`/`updated_at` instead of being normalized to the required ISO 8601 format. These fields are now always correctly formatted.
* Fixed the seller rail's "Set up" section sometimes collapsing itself a moment after loading, hiding rows ("Get ready to sell", "Ad server", "Sales agent", "Branding & distribution", and others) that had just appeared — its collapsed/expanded state could flip once the readiness read settled after the section had already rendered open. The section now defaults to collapsed while that read is still pending, so it either stays put or reveals itself once settled, and never opens a row only to close it out from under you. Explicitly expanding "Set up" while it loads is preserved even if the section would otherwise default to collapsed once loading finishes.
* Seller admins can now download a printable Discovery Card PDF from Branding & distribution after publishing a card. The PDF is generated from the current published revision for manual printing; fulfillment, provider submission, GCS delivery, webhooks, and durable print orders remain out of scope. This is seller-only tooling; buyers do not see a new download action.
## 5.337.0 — August 31, 2026 at 7:20 PM UTC
* When creating a wholesale product in the inventory-selector widget, you can now set delivery type (guaranteed or non-guaranteed) and a fixed CPM rate. On the product creation form, select "Guaranteed (fixed price)", then enter a positive CPM amount and the 3-letter currency code that matches your storefront's settlement currency. Guaranteed products are validated and submitted with the correct ad-server settings automatically - no manual configuration outside the widget. Existing products are unaffected. Buyer-facing inventory availability is unchanged: buyers see guaranteed products when they were previously configured correctly, now with less risk of misconfiguration on the seller side.
* **For buyers:** Third-party impression trackers configured on an advertiser or campaign (via `PUT /api/v2/buyer/advertisers/:id/tracking-config`) now attach automatically to outbound display and HTML creatives on the next creative sync or media buy. The media buy ID is stamped into each pixel request for per-buy attribution. No buyer action is required beyond configuring the tracker.
* If a creative's format does not support tracker attachment (for example, a format that reserves all asset slots), the tracker is skipped for that creative and a `NO_ATTACHMENT_PATH` event is logged. The buy proceeds normally; only that creative's measurement is affected.
* Trackers requiring IAB TCF vendor-scoped consent must include a `gvl_vendor_id` in the tracking configuration. If omitted, the IAB consent macro resolves to an empty string in the pixel URL, meaning the pixel fires without a consent signal. VAST (video) tracker support is coming in a separate update; existing click tracking is unaffected.
* **For sellers:** Buyers whose advertisers or campaigns have trackers configured will now include `
` pixel tags (injected before `