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

# Campaigns on connected seller accounts (alpha)

> Track and, when separately enrolled, author campaigns on a connected seller or ad platform

<Warning>
  Connected-account campaigns are an alpha feature available only to enrolled
  design partners. Enrollment is one customer-facing capability; scoped
  internal kill switches may still protect connected-account
  writes and the external AdCP buyer edge independently. The alpha is API and
  developer-documentation only. Do not depend on change webhooks or
  scheduled-delivery integration yet.
</Warning>

## Tracked and managed

Connecting a seller account puts its campaigns on the platform in one of two
`management` states:

* **`tracked`** — a campaign Interchange did not set up. On sync, the platform
  materializes a read-only projection of the seller's own campaign and keeps it
  fresh: the shell is derived from the media buys underneath it and updates
  automatically as the seller changes things. Interchange supplies tenancy,
  identity, audit, governance configuration, and reporting access around it; it
  never acts on it.
* **`managed`** — a campaign authored or adopted through the platform spine.
  For separately write-enrolled accounts, Interchange forwards an explicit
  media-buy create or update and then reconciles from the provider response.
  Campaigns authored this way — including by an external AdCP client
  addressing one storefront — are managed from birth.

In both states the seller's execution system is the source of truth; the
platform reflects it. Instructions flow down; truth flows up.

The compatibility API value for both is `mode: "directed"` — a deprecated wire
discriminator, not a product name, kept while the alpha cohort migrates to
`management`. A discovery campaign that happens to finish with one seller
remains an ordinary managed campaign; Interchange performed the selection.

Every campaign response carries one `mode` and one `management` state:

| Mode          | Who steers                           | Shape                                                       |
| ------------- | ------------------------------------ | ----------------------------------------------------------- |
| `discovery`   | Interchange, from a buyer brief      | One campaign can create media buys with one or more sellers |
| `performance` | Interchange, from an objective       | Interchange allocates across sellers toward the objective   |
| `directed`    | The addressed storefront or platform | Exactly one campaign, one media buy, and one seller         |

The directed invariant is one-to-one: the storefront campaign is one AdCP media buy
and one Interchange campaign shell. The shell's name, status, flight, and budget are
projections of the buy, not independently editable values. The projection preserves
both media-buy identifiers: Interchange's `media_buy_id` and the storefront's
`adcp_media_buy_id`.

This includes an external AdCP client calling one Interchange-hosted storefront. The
storefront manages execution; Interchange records the contract, governance, reporting,
and ledger envelope. The campaign still returns `mode: "directed"`. With the
buyer/storefront campaign-spine enrollment active, later `update_media_buy` and
cancellation calls enter the same durable directed-write journal before the storefront
is called. Accepted package-budget changes activate a new SCD2 version; accepted
cancellation releases the governed exposure. Adding packages on this endpoint path is
not yet supported.

Every directed campaign also returns explicit provenance:

* `directed.provenance: "connected_account"` includes connection/account ids,
  provider, mirror freshness, and the upstream media-buy id.
* `directed.provenance: "storefront_endpoint"` includes the addressed storefront,
  the external buyer's media-buy id, and Interchange's media-buy id. It does not
  expose mirror state because this path is not an account mirror.

The same campaign delivery endpoint dispatches by that provenance. Connected-account
campaigns read through the subscribed provider gateway; storefront-endpoint campaigns
read through the addressed storefront, including direct social/ad-platform adapters.

## Connect and subscribe an account

First [connect the official adapter](/v2/guides/connecting-ad-platforms) and
[map its discovered ad account to an advertiser](/v2/concepts/ad-platform-accounts#after-connecting).
For an agent-driven setup, the complete sequence is:

1. `list_storefronts` to find the official adapter, then `connect_storefront` to
   obtain the human OAuth handoff URL;
2. `list_storefront_connections` and `list_storefront_connection_accounts` to
   select a buyable account returned by that connection;
3. `list_storefront_connection_account_mappings` to read its mapping state,
   then `map_storefront_connection_account_to_advertiser` with the advertiser
   ID if the selected account is not mapped to the intended advertiser. The
   connection/account path identifies an official adapter; do not copy its
   storefront `platformId` into `sourceId`;
4. `subscribe_directed_campaigns` to start its mirror.

This account-selection step applies to operator-built social adapters, which can use
only an existing platform account. Agent-built storefronts may instead sync/reuse an
existing account or establish a new one. Account setup does not determine campaign
mode: a direct buy is directed in either case, and a new account simply has no history
to backfill.

Organization/manager and publisher-identity accounts cannot be subscribed. Then
subscribe the mapped ad account:

```bash theme={null}
curl -X POST \
  "https://api.interchange.io/api/v2/buyer/storefront-connections/42/accounts/81/directed-campaigns/subscribe" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "advertiserId": "12345"
  }'
```

For official adapters, `sourceId` is optional and the canonical provider is
derived from the connection. External AdCP storefront sources still pass their
inventory-source `sourceId`.

Subscription immediately attempts a mirror sync. The adapter's
`get_media_buys` operation is called without media-buy IDs so the account can
be enumerated. Active campaigns are always included; terminal history is
backfilled for one year by default. Subsequent worker sweeps reconcile changes
every 15 minutes by default.

Broad account enumeration is not treated as proof that a previously mirrored
campaign was deleted. If a known upstream ID is omitted, Interchange reads that
ID through a bounded, resumable direct-read cycle before treating absence as
authoritative. A returned terminal campaign remains available as a canceled or
completed mirror, including its prior packages and latest revision; an empty
terminal package response does not erase package history. Already-terminal
local mirrors do not require repeated provider reads. If a direct response fails
or omits any requested ID, the sync reports `ERROR` and retains the last valid
mirror. The one-year boundary limits new historical campaigns; it does not
suppress a later terminal transition for a campaign already mirrored.

A refresh for one campaign prioritizes that campaign inside the bounded
direct-read cycle, so large connected accounts do not delay its readback behind
the background continuation cursor.

The subscribe request returns after the initial enumeration attempt. Its response
contains `backfillStart`, `lastSyncedAt`, `lastSyncStatus`, `lastSyncError`, and
the mirrored/retired/skipped counts. Read the same account-level state later
with `GET /api/v2/buyer/storefront-connections/{connectionId}/accounts/{accountId}/directed-campaigns/subscription`.
Campaign lists and the campaign widget show the mode, provider, mirror state,
and `directed.lastSyncedAt` for each mirror. Active mirrors appear in the
default campaign list (the live working set); mirrored history — completed,
paused, or canceled campaigns — requires an explicit `status` filter or
`status=ALL`; see [Relationship rollup](#relationship-rollup).

On a failed reconciliation, `lastSyncError` and the single `sync.errors` entry
use the fixed safe summary `Connected provider account could not be
synchronized.` Use `errorCode`, `errorField`, `errorReason`, and `upstreamCode`
for any available machine-readable diagnosis. Provider messages, request IDs,
account IDs, and media-buy IDs are never returned in these error fields or
persisted as the subscription error.

`backfillStart` is the fixed metadata-history boundary, not a running progress
percentage or completion timestamp. Enrollment is durable even if enumeration is
unsupported or temporarily unhealthy; in that case the subscription reports status
`ERROR`, `lastSyncError`, and zero reconciliation counts while other supported directed
operations remain available. The one-year backfill does not pre-fetch delivery reports.

To stop mirroring, delete the subscription. Interchange pauses its sweep and
retires the local mirror shells; it does not delete campaigns from the connected
platform. Status and unsubscribe remain available if alpha exposure is removed.

Governance caps are stored with the subscription for directed writes. Phase 1
read enrollment does not grant write authority. Writes require the separate
`directed-campaign-writes` customer gate plus `maxMediaBuyBudget`,
`maxAccountBudget`, and `currency`; missing or inconsistent governance fails
before a provider call. These values are set by an Interchange operator after
pilot authorization. Ordinary buyer callers cannot set or raise them by
subscribing again.

### Registered AdCP storefront sources

An enrolled buyer can use the same connected-account campaign surface with a
registered third-party AdCP storefront source. First register source credentials,
call `list_available_accounts`, and map the returned account to an advertiser.
Then call `connect_adcp_storefront` to project that existing relationship into
the shared connection/account plane:

```bash theme={null}
curl -X POST \
  "https://api.interchange.io/api/v2/buyer/storefronts/42/sources/publisher-sales/adcp-connection" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "advertiserId": "12345",
    "accountId": "seller-account-7",
    "credentialId": "81"
  }'
```

The response returns `connectionId` and `connectionAccountId`; use those values
with the normal subscribe, product, campaign, and delivery operations. Official
adapters dispatch through their delegated adapter credential. Registered AdCP
sources dispatch through the AdCP SDK and the mapped buyer account. Identity,
governance, mirroring, idempotency, metering, and reconciliation are shared.

Every active storefront can serve directed mode. Connection records the
seller's reported tools as readiness evidence rather than requiring a separate seller
flag. Each call still fails closed if the source, pinned credential, connection
account, or advertiser mapping is no longer active. An unsupported operation is
reported for that operation without hiding the storefront's other capabilities.

For registered sellers, a snapshot is complete only when every requested page
returns an explicit boolean `pagination.has_more` and every continuation cursor
advances. Every media buy must include a valid ISO currency; when it echoes an
account, that `account_id` must equal the pinned connection account. Invalid or
incomplete snapshots never retire existing mirrors. Sellers that omit the optional
account echo must prove account-scoped completeness before their snapshots can retire
existing mirrors.
Previously mirrored nonterminal IDs omitted by a broad snapshot are also read
directly in bounded, lease-renewed batches. A persisted cursor resumes large
accounts, and every direct batch must return the complete requested set before
the sync can proceed.
Budget and package updates require a provider revision observed by the latest sync;
revisionless sources remain eligible only for safe operations such as pause or cancel.

This alpha operation does not accept an arbitrary endpoint URL or raw secret.
The seller source must already be registered on an active storefront. Endpoint
ownership, seller identity, and `adagents.json` authorization must be established
before a URL can become a transacting relationship.

## Alpha operation readiness

| Source                                        | Directed availability                              | Operation readiness                              | Notes                                                                                                                                                                                                                                                                                            |
| --------------------------------------------- | -------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| TikTok official adapter                       | Available when the customer flag is on             | Live-proven mirror/delivery; capped write canary | Account-wide enumeration and one-year delivery read-through are verified. Paused create, pause, and cancel are enabled. Resume and all other updates fail closed until the post-canary capability review.                                                                                        |
| Meta official adapter                         | Available when the customer flag is on             | Capped no-spend lifecycle canary                 | Explicitly paused creates keep the campaign and every package paused. Finite-flight budgets use the account currency's lifetime-budget units. Only revision-guarded package-budget decreases, pause, and cancel are accepted; resume, schedule changes, package adds, and increases fail closed. |
| Other official sales adapters                 | Available when the customer flag is on             | Measured per operation                           | A conservative paused-create and pause/cancel profile is attempted through the typed adapter. Unsupported tasks return their own errors and feed adapter-readiness backlog.                                                                                                                      |
| Registered third-party AdCP storefront source | Available when active and mapped                   | Measured per operation                           | Uses `connect_adcp_storefront`; canonical AdCP tasks dispatch through the SDK and preserve the source's own errors.                                                                                                                                                                              |
| Modular AdCP sources, including Citrus Ads    | Available through their active storefront endpoint | Measured per operation                           | Modular-source implementation remains separate; it does not change the campaign mode or gate other storefronts.                                                                                                                                                                                  |

The service uses an adapter-neutral AdCP task contract. Readiness describes which
operations have passed live end-to-end verification; it is not a product allowlist.

## List and inspect mirrors

Use the normal campaign surface and filter by mode:

```bash theme={null}
curl \
  "https://api.interchange.io/api/v2/buyer/campaigns?mode=directed&advertiserId=12345" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

`GET /api/v2/buyer/campaigns/{campaignId}` returns the same campaign resource
as other modes, including the single mirrored media buy and its upstream
packages. Discovery-only operations such as `execute` and product auto-selection
always reject directed campaigns. In mirror-only enrollment, pause/reactivate and
ordinary field updates also reject. Buyers separately enrolled in the write alpha
must use the explicit `mode: "directed"` create or update branch described below;
unsupported provider mutations continue to fail closed.

To pull a fresh upstream snapshot without changing it, send
`{ "mode": "directed", "refresh": true }` to the normal campaign update
endpoint. This re-enumerates the subscribed account; it does not write to the
seller.

A `brief` is never valid for `mode: "directed"`; it is a validation error, not
an ignored field.

When the buyer starts with an advertiser brief, call
[`plan_directed_campaign`](/v2/buyer/campaigns/tasks/plan-directed-campaign)
before create. The preflight returns controlled provider decisions or explicit
clarification requirements without changing the provider. Accept a ready
proposal, then pass its exact reference on an assisted create. The subsequent
create still contains only reviewed, explicit AdCP fields and never accepts a
hidden brief.

## Relationship rollup

Scale lives in the relationship, not the campaign list. Tracked campaigns never
appear as ambient rows; the connected-account relationship carries the rollup
and drill-in enumerates on demand.

Each account on `GET /api/v2/buyer/storefront-connections/{connectionId}/accounts`
returns an optional `campaignRollup` object:

| Field              | Meaning                                                                                                  |
| ------------------ | -------------------------------------------------------------------------------------------------------- |
| `campaignsTracked` | Non-retired tracked campaigns on the account (campaigns the platform did not set up, mirrored read-only) |
| `campaignsActive`  | Subset of `campaignsTracked` currently active                                                            |
| `trackedSpend`     | Delivered spend across tracked campaigns over the trailing window                                        |
| `managedSpend`     | Delivered spend across managed campaigns (authored through the platform) over the trailing window        |
| `managedShare`     | The adoption metric: `managedSpend / (trackedSpend + managedSpend)`; `null` when no spend was accounted  |
| `currency`         | ISO 4217 currency every spend value is denominated in — always the account currency                      |
| `windowDays`       | Trailing window the spend aggregates cover (365)                                                         |

Spend is seller-reported net in the account currency: directed buys dispatch the
full media budget zero-fee, so net equals the buyer's budget. Rollups are never
summed across currencies — the rollup is per account, and it is omitted when the
account currency is unknown or the account has no live campaign mirrors.

The connection rows on `GET /api/v2/buyer/storefront-connections` carry counts
only: `campaignsTracked` and `campaignsActive` across the connection's
accounts. Money never rolls up to the connection level, because accounts on one
connection can settle in different currencies.

## Create and update through the connection

<Warning>
  Upstream writes are a narrower alpha. `FEATURE_NOT_ENABLED` means the customer
  has mirror access but is not write-enrolled. Never bypass the campaign API by
  calling an adapter directly.
</Warning>

For an explicit buyer-authored plan, first call
`list_directed_campaign_products` with the mapped connection/account path and
`advertiserId` query. It reads the provider's no-brief product catalogue and
returns the exact `product_id` and `pricing_option_id` values accepted by that
account. Do not use ordinary discovery output from another seller and do not
invent provider IDs.

```bash theme={null}
curl \
  "https://api.interchange.io/api/v2/buyer/storefront-connections/42/accounts/81/directed-campaigns/products?advertiserId=12345" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

This catalog read does not create a discovery session or apply a brief. TikTok
returns all six static products and skips historical scoring and recommendation
ranking when called through this path.

TikTok's ordinary AdCP `get_products` discovery, including when Murph uses it to
explore campaign options, is a separate read-only path. For a non-empty brief,
it returns runnable recommendations only when the buyer affirmatively names one
outcome. Otherwise Murph asks the buyer to choose reach, video views, traffic,
conversions, app installs, or lead generation and returns no products or
proposal. Reply with an explicit choice such as `Objective: traffic` and retry
the Murph request. This does not enable TikTok in the named
`plan_directed_campaign` operation, which currently supports Meta only, and it
does not authorize a provider write.

Create a directed campaign through the normal campaign endpoint. The request is
an explicit AdCP media buy: there is no brief, discovery session, or execute
step. `connectionId`, `accountId`, `idempotencyKey`, and `currency` live at the
campaign-request root so account routing, replay protection, and governance
cannot be overridden inside `mediaBuy`.

Every create records decision provenance. Use `decisionMode: "raw"` for an
explicit buyer-authored pass-through; omission remains backward-compatible and
also defaults to raw. Raw writes cannot claim Scope3 brief strategy. A Meta
brief-assisted create instead uses `decisionMode: "assisted"`
and the exact accepted reference from
[`accept_directed_campaign_plan`](/v2/buyer/campaigns/tasks/accept-directed-campaign-plan).

```bash theme={null}
curl -X POST \
  "https://api.interchange.io/api/v2/buyer/campaigns" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "directed",
    "advertiserId": 12345,
    "connectionId": "42",
    "accountId": "81",
    "name": "TikTok reach canary",
    "idempotencyKey": "buyer-2026-07-canary-001",
    "decisionMode": "raw",
    "currency": "USD",
    "mediaBuy": {
      "brand": { "domain": "example.com" },
      "total_budget": { "amount": 60, "currency": "USD" },
      "packages": [{
        "product_id": "tiktok_reach",
        "pricing_option_id": "tiktok_reach_cpm",
        "budget": 60,
        "targeting_overlay": {
          "geo_countries": ["US"],
          "frequency_cap": {
            "max_impressions": 3,
            "window": { "interval": 7, "unit": "days" }
          }
        }
      }],
      "start_time": "2026-07-20T00:00:00Z",
      "end_time": "2026-07-21T00:00:00Z",
      "paused": true
    }
  }'
```

The TikTok canary requires `paused: true` and rejects creative assignments or
inline creatives server-side until the upstream object, mirror, governance, and
delivery reads are verified. Use future-dated campaigns as an additional
operating precaution.
TikTok packages can include canonical AdCP `catalogs[]` entries of type
`offering` or `job`; the adapter expands their items into provider-native
promoted offerings. Other catalog types return `CAPABILITY_NOT_SUPPORTED`
before the provider write begins.
Every TikTok package must include a non-empty
`targeting_overlay.geo_countries` array using a country supported by the
provider catalog. Interchange converts those ISO country codes to TikTok
locations and uses the explicit `PLACEMENT_TIKTOK` provider placement; missing
geo fails before the provider write begins. Interchange disables TikTok's
automatic Search Results toggle and does not select an account-specific
subplacement. Lemon8, Search Feed, Pangle, and Global App Bundle are not
advertised as writable inventory. TikTok for Web, Profile Feed, and Following
Feed are not separate API selections in this flow.
The community catalog keeps the stable buyer-facing placement ID
`tiktok_for_you_feed`; Interchange maps it to the provider's
`PLACEMENT_TIKTOK` enum internally.
TikTok Reach packages also require an explicit
`targeting_overlay.frequency_cap`. `max_impressions` must be from 1 through
1000, and the `window.interval` must be from 1 through 30 with `unit: "days"`.
Interchange validates those provider bounds before creating anything in
TikTok and forwards the buyer-selected cap unchanged.
`currency` must match both the subscription governance currency and
`mediaBuy.total_budget.currency`. The total and package budgets must stay under
the stored per-buy and authored-account caps. TikTok's campaign minimum applies
to the net media amount after fees; see
[Provider minimums apply after fees](/v2/concepts/budgets-and-fees#provider-minimums-apply-after-fees).

Meta packages may use `targeting_overlay.geo_regions` with ISO 3166-2 codes such
as `AU-NSW`. Interchange resolves each subdivision to Meta's native region ID
before creating the campaign and returns the canonical ISO code on readback. If
Meta cannot resolve exactly one region in the requested country, creation stops
before any provider campaign is created. When a package includes both a region
and its parent country, the region is authoritative; Interchange removes that
parent country from Meta targeting so delivery is not broadened nationally.

For Meta, every finite-flight package is written as an authoritative
`lifetime_budget` using the selected ad account's ISO 4217 minor unit. A native
Meta campaign that exposes only `daily_budget` cannot be interpreted as total
exposure, so mirror and update operations fail closed until it has an
authoritative lifetime budget. Values that cannot be represented exactly in the
currency's minor unit are rejected before Meta creates a campaign. A controlled
Meta update may decrease each identified package budget under the latest
provider-derived revision; one package cannot increase even if another package
decreases by the same amount. Schedule changes, resume, and package additions
remain unavailable during this canary stage.

Meta packages may reference stable `creative_ids` that were already synced to
the selected account on raw writes. Assisted decision-plan v1 rejects inline
creative execution; creative attachment remains unavailable for assisted v1
until the separate sync/assignment flow lands. A sync requires the legacy Meta `format_id` returned by
`list_creative_formats`. Scope3 auto-selects the only Facebook Page authorized
for the selected ad account; when several are authorized, provide one of the
named `page_id` choices returned by zero-write validation. When needed, the
validation message tells you the next `ext.meta_page_selection_offset` to send.
An explicit `page_id` or
optional `instagram_actor_id` must belong to the selected account and is
validated before provider writes. Single-image and video assets must be reachable at public
HTTPS URLs and satisfy the declared format's roles, file types, dimensions,
aspect ratio, duration, and size constraints. Interchange validates the supplied
roles and metadata before writing. Image downloads additionally pin public DNS,
reject redirects, cap bytes, and verify JPEG/PNG content; Meta performs final
provider-side processing. Interchange does not ingest a host-local file or
upload assets from an MCP host; host the asset first. Carousel
child attachments, native localization/translation, and sync-time assignment or
delete mutations are not supported. Creative-bearing creates remain paused.
Interchange reports success only after Meta reads the ad back from the selected
account, campaign, and ad set with the requested creative and paused status. A
pending review status is still not a claim that the ad is launch-ready or that
no Ads Manager follow-up will be required; that claim requires affirmative
provider approval for the same ad.

Every Meta package must also include explicit geographic targeting. Country
values are normalized as ISO 3166-1 alpha-2 codes (`UK` is accepted as an alias
for `GB`); a missing country, invalid code, or postal area without its own
country fails before Meta creates a campaign. Region strings, free-form
interest themes, and custom-audience signals are not applied until they can be
resolved and round-tripped without losing provider targeting semantics.
After creating a paused ad set, Interchange reads Meta's stored targeting back
and requires exact normalized country, region, city, and postal equality before
reporting success. A mismatch deletes the campaign and returns an error.
Campaign package responses expose representable geography and device choices as
`targetingOverlay`; `providerTargeting` carries the normalized provider country,
region, city, postal, age, and gender readback. For a package created through
Interchange, `requestedTargeting` retains the canonical request and
`targetingMatchesRequest` reports whether the current provider-normalized value
still equals it. This makes a later Ads Manager edit visible without rewriting
the original request. Provider-authored campaigns created outside Interchange
do not receive an invented request, so those two fields are omitted.

When mirroring an existing Meta account, Interchange also checks for native
targeting constraints that the directed-campaign projection cannot represent
without losing meaning. A nonempty excluded geography, locale, interest,
behavior, included or excluded custom audience, publisher platform, or
Facebook/Instagram placement makes the account sync fail instead of returning a
partial package. The previous complete mirror remains available and the
subscription reports an error until every native constraint can be represented
or is removed. City-radius or distance-unit proximity and unknown Meta targeting
dimensions fail the same way.

Meta does not offer an atomic compare-and-swap for these fields. Interchange
assigns a durable monotonic revision whenever the provider campaign or ad-set
state fingerprint changes, including multiple changes in the same second. The
token remains an optimistic preflight: a native edit after the read and before
the write can still race it. Run the no-spend canary only in its dedicated
account, refresh before operator changes, and treat a revision conflict as a
requirement to reread rather than retry blindly.

Meta directed writes begin a new durable revision domain with this capability.
Revisions are opaque: always use the value from the latest read, never compare
their magnitude or reuse an experimental token observed before Meta directed
writes became available.

Update the same object through the normal campaign update endpoint. The URL
provides both the local campaign identity and its upstream media-buy identity;
do not repeat `account`, `media_buy_id`, `idempotency_key`, or `revision` inside
`mediaBuy`.

```bash theme={null}
curl -X PUT \
  "https://api.interchange.io/api/v2/buyer/campaigns/campaign_123" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "directed",
    "idempotencyKey": "buyer-2026-07-cancel-001",
    "currency": "USD",
    "expectedRevision": 1,
    "mediaBuy": {
      "canceled": true,
      "cancellation_reason": "Canary completed"
    }
  }'
```

A confirmed synchronous provider result returns the projected `campaign` plus a
`directedWrite` block whose status is `CONFIRMED`. If the provider accepted the
operation but the mirror cannot yet prove it, the endpoint returns `202 Accepted`
with the write result's status `AWAITING_CONFIRMATION` and a possibly null
`campaignId`; poll `get_campaign` or run the read-only refresh branch. Replaying
the same idempotency key and payload returns the recorded result. Reusing the key
for a different payload is a conflict, and an ambiguous provider outcome is never
automatically dispatched a second time.

Connected-account responses keep the two identifiers separate:
`directed.upstreamMediaBuyId` is the platform's campaign identifier and
`directed.mediaBuyId` is Interchange's internal media-buy identifier used by the
embedded `mediaBuys[]` entry. The embedded media buy also returns its
provider-derived `revision` and currently callable `validActions`; do not compare
the internal identifier to the upstream identifier.

If the seller accepts an update but Interchange cannot finish the local campaign
projection, the write is recorded as ambiguous rather than left dispatching. New
mutations stay blocked until that accepted result is reconciled; the seller request is
not sent again automatically.

Some platforms, including TikTok, omit a deleted campaign from subsequent complete
account snapshots instead of returning a terminal row. When that authoritative
absence follows a pending cancellation for the known mirror, Interchange confirms
the operation, records the projected buy as canceled, and keeps its terminal shell
readable so the response or an identical idempotent retry can report `CONFIRMED`.
The same absence never confirms an unrelated write. After cancellation, reuse only
the original idempotency key to read its result; a new mutation is rejected before
the provider is called. The retained campaign shell reports `status: CANCELED`, as does
its projected media buy.

The sync sweep marks any write left dispatching for more than five minutes as
ambiguous. Updates can still reconcile through their known upstream ID. For a
create with no returned upstream ID, inspect the connected account before taking
any further action and do not retry with a new idempotency key.

Product enumeration and directed create/update usage are recorded as dark,
platform-borne usage with zero billable amount during the alpha. Each provider
dispatch is counted once; a later async confirmation does not add a second usage
event.

TikTok updates use the latest provider-derived revision to reject a stale write
before changing the platform. Supported changes are campaign pause/resume,
package budget, bid, end time, and media-buy cancellation. Package cancellation,
new packages, start-time changes, and other package fields remain unsupported.
Interchange bounds each request to 20 package mutations, pauses and verifies an
enabled campaign before child writes, and reads the complete provider hierarchy
back before reporting success. Reversible partial failures are restored to the
pre-write provider values. If that restoration cannot be verified, Interchange
makes one final verified disable attempt and reports an unrecovered partial
update; the error states when provider containment itself cannot be verified and
manual reconciliation is required.
TikTok creation also fails closed at each provider boundary. Interchange verifies
that the campaign, every ad group, and every ad belong to the selected advertiser,
have the expected parent, and remain paused before activation. It activates
children before the campaign, validating each response identity and enabled-state
readback before progressing. If activation or later readback fails, cleanup never
mutates an ownership-unconfirmed object. For an owned campaign, cleanup first
disables the exact campaign and confirms its paused readback before requesting
deletion. The result is reported as rolled back only after TikTok returns terminal
deletion. If deletion cannot be confirmed after the pause is confirmed, the result
is `contained_paused` and requires operator reconciliation without being marked
possibly active. If the pause itself cannot be confirmed, the result remains
possibly active and requires reconciliation. Offering-based TikTok packages can
currently be created only while paused because the adapter cannot yet materialize
offering assets as runnable ads; use `creative_assignments` for delivery-ready
packages.
For every new ad group, Interchange also reads back `placement_type`,
`placements`, `tiktok_subplacements`, and `search_result_enabled`. The campaign
is deleted while paused if TikTok broadens, filters, omits, or changes that
placement contract. Provider-native campaigns that use automatic placement,
partner inventory, allowlisted subplacements, or Search Results remain visible
in account inventory but are read-only through Interchange.
Cancellation is terminal only after a bounded full-hierarchy poll confirms
TikTok campaign deletion. This path remains in live-canary readiness rather
than Green production evidence.

## Read delivery through the seller

Delivery is read through live from the connected platform's
`get_media_buy_delivery` operation:

```bash theme={null}
curl \
  "https://api.interchange.io/api/v2/buyer/campaigns/campaign_123/delivery?startDate=2026-01-01&endDate=2026-07-10" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

When dates are omitted, `startDate` is bounded to the subscription's one-year
backfill window and `endDate` defaults to yesterday because same-day platform
reporting is commonly incomplete.

Every delivery request calls the connected platform. Interchange returns that
response to the caller but does not persist or later serve the raw delivery
payload or its time series in Phase 1. It persists only the latest aggregate
media-buy and package metrics together with the exact requested
`windowStart`/`windowEnd` and refresh time, so the campaign projection can label
what period its snapshot represents. Subscription and the 15-minute metadata
sweep do not pull delivery automatically.

Provider reporting limits are handled inside the adapter. TikTok daily ranges
longer than 30 days are read as consecutive windows and combined into one
delivery response, so the caller can request the full one-year window.
Snap delivery verifies the selected account and its ISO currency, enumerates
the complete ad-squad inventory, and reconciles package metrics to the campaign
report for the exact requested window. Empty packages remain visible at zero;
malformed, partial, duplicate, cross-campaign, or non-reconciling evidence
fails the read rather than returning a partial snapshot.

## Errors and recovery

* `FEATURE_NOT_ENABLED` means the customer is not enrolled in the requested
  read or write alpha. Mirror access does not imply write access. There is no
  environment-variable or direct-adapter bypass.
* If subscribe reports that the account is not mapped, re-list the official
  adapter connection accounts and mappings, map the account to the intended
  advertiser, then retry with only IDs returned by those list operations.
* If `lastSyncStatus` is `ERROR`, show `lastSyncError` and `lastSyncedAt` as the
  time of that failed attempt. A malformed or partial upstream snapshot fails
  closed and retains the previous mirrors. Reconnect first for authentication
  errors; otherwise retry a read-only refresh after the provider recovers.
* A successful sync with `mirrored: 0` is an empty eligible metadata window,
  not a failed reporting backfill.

## Alpha boundaries

The directed-campaign alpha includes account subscription, campaign mirroring,
a one-year historical window, delivery read-through, and separately enrolled
create/update canaries. It does not include:

* buyer webhooks for upstream changes;
* including mirrors in managed scheduled-delivery exports;
* public website positioning or general availability.

Write enrollment remains per customer and per verified provider. Citrus Ads and
other modular sources remain separate from the native social-adapter canary.
