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

# Connecting Ad Platform Accounts

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

<Note>
  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).
</Note>

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

<Note>
  `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.
</Note>

<Note>
  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.
</Note>

## 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`, `ads_read`, and
`business_management`. Page-backed ads also request `pages_show_list`,
`pages_read_engagement`, `pages_manage_ads`, and `instagram_basic`; Instant
Form discovery also requests `leads_retrieval`. Reconnect an existing Meta
connection after these Page, Instagram, 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

<Warning>
  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`.
</Warning>

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
`<platform>:<capability>` 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 `meta_image_feed` creative assigned
to one existing paused package. Existing-post and video assignments are in
Testing and are unavailable through standard buyer workflows. Apostra verifies
that the package and its campaign
belong to the selected account and are paused before creative writes, creates a
paused Meta ad, 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. Only a fully proven result returns `ext.manual_action_count: 0`
alongside `scope3_meta_assignment`. 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. Treat the presence of
`ext.manual_action_count: 0`, not membership in `assigned_to`, as the signal
that no Ads Manager follow-up remains. 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, multiple sync-time 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.

### 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": "<returned-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.
