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

# Create media buys from a product query

> Stage or execute several storefront-qualified proposals and products through one draft campaign cart

`POST /api/v2/buyer/media-buys/batch`

MCP tool and `api_call` operation: `create_media_buys`

This operation continues from the `ext.interchange.execution_id` returned by
[`get_products`](/v2/buyer/discovery/multi-storefront-get-products). It can
select inventory from several storefronts in one request. Apostra creates
or updates one DRAFT campaign cart, then prepares one or more media buys grouped
for bilateral seller execution.

## Preserve Buyer Proposal JSON identifiers

Buyer Proposal JSON is a separate continuation into a seller's bilateral AdCP
`create_media_buy` operation. Its published shape and field set stay the same,
but `productId` may be a source-qualified `wh:` identifier and
`pricingOptionId` may be a source-qualified `whpo:` identifier. Pass both values
to `create_media_buy` exactly as returned. Do not remove the prefixes or replace
either value with another catalog identifier; the source qualification keeps
identical upstream IDs from different sources distinct.

The `create_media_buys` batch operation on this page instead selects products
and proposals from the durable `get_products` execution identified by
`productQueryId`; it does not accept a standalone Buyer Proposal JSON document.

## Request

```json theme={null}
{
  "productQueryId": "query_123",
  "campaign": { "campaignId": "campaign_123" },
  "mode": "execute",
  "replace": false,
  "debug": false,
  "selections": [
    {
      "kind": "proposal",
      "proposalId": "sfp1:42:...",
      "channelGroupId": "ctv",
      "totalBudget": 40000
    },
    {
      "kind": "product",
      "productId": "sf1:57:...",
      "channelGroupId": "mobile-web-display",
      "pricingOptionId": "cpm-standard",
      "budget": 10000,
      "bidPrice": 8.5
    }
  ]
}
```

| Field            | Required | Description                                                                                                                         |
| ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `productQueryId` | Yes      | Exact `execution_id` returned by `get_products`.                                                                                    |
| `campaign`       | Yes      | `{campaignId}` for an existing cart, or `{create: {...}}` containing a complete discovery-mode campaign create body.                |
| `mode`           | Yes      | `stage` prepares DRAFT media buys without seller calls; `execute` also dispatches them.                                             |
| `selections`     | Yes      | One to 100 qualified proposal/product selections. Add `channelGroupId` to every selection when the campaign defines channel groups. |
| `replace`        | No       | Replaces the existing cart selections before applying the batch. Defaults to `false`.                                               |
| `debug`          | No       | Includes execution diagnostics on failures when `mode` is `execute`. Defaults to `false`.                                           |

Proposal selections distribute `totalBudget` using the seller's allocations.
Omit `totalBudget` only when the proposal carries a recommended total. Product
selections are for proposal-less products and may include `pricingOptionId`,
`budget`, `bidPrice`, `targetingOverlay`, `pageId`, `pixelId`, `instagramActorId`, and `welcomeMessageFlowId`.

`channelGroupId` assigns a selection to one campaign channel group. Interchange
creates separate media buys for different groups, even when seller and currency
match. The ID must name a group on the campaign, and every selection must carry
one when the campaign defines groups. The client remains responsible for
choosing products that satisfy the group's declared inventory dimensions; see
[Channel groups](/v2/buyer/campaigns/channel-groups).

`pageId` is the Meta Facebook Page ID — required when the connected ad account has more than one authorized Page; auto-selected when exactly one Page is authorized.

`pixelId` is the Meta Pixel / Dataset ID for conversion tracking — required for all Meta Sales (OUTCOME\_SALES) products. Unlike `pageId`, there is no auto-select: every Meta Sales buy must explicitly specify a pixel.

`instagramActorId` is the Meta Instagram account ID — required for Instagram Direct destinations (Instagram Direct Conversations, Instagram-based Lead Generation) when the connected ad account has more than one authorized Instagram account; auto-selected when exactly one is authorized.

`welcomeMessageFlowId` is the Meta welcome message flow ID — required for lead-to-message destinations (Messenger, Instagram Direct, and WhatsApp Lead Generation). There is no auto-select, even with a single candidate; pass one of the welcome-message-flow candidates returned by account resource readiness in the discovery/`get_products` response. Candidates only appear on a catalog-style `discover_products` call with no `brief` — a `buying_mode: "wholesale"` request that still includes a `brief` does not surface them; a natural-language brief call surfaces readiness for at most the one product the brief resolved to, and omits it entirely if the brief needed any clarification.

`pageId`, `pixelId`, `instagramActorId`, and `welcomeMessageFlowId` are all fixed at ad creation and cannot change after a media buy leaves DRAFT status.

When creating the campaign inline, do not set `productIds`; `selections` is the
only cart mutation in this request. If `discoveryId` is present, it must equal
`productQueryId`. The campaign and product query must belong to the same
advertiser. New selections can only be staged into a DRAFT campaign; an ACTIVE
campaign is accepted only for an unchanged execution retry.

The target campaign must be a discovery campaign. A performance campaign's
product mix is selected and allocated by the platform toward its objective, so
manual batch selections against one fail with a validation error — on stage
and on execute alike. Make manual selections on a discovery campaign instead.

Proposal selection also requires the durable accepted-proposal set written by
the current `get_products` flow. If an older execution predates that set, run
`get_products` again; Apostra will not infer acceptance from the raw seller
response.

## Choose stage or execute

When Murph continues a progressive product query, it waits for complete results
before attachment and retains the original query identity. An incomplete or
ambiguous continuation blocks automatic attachment; an exact confirmed retry is
acknowledged without dispatch. See
[Murph continuation and attachment](/v2/buyer/discovery/tasks/browse-products#response)
for its bounded wait and recovery behavior.

Use `mode: "execute"` as a one-call shortcut when the selected products can be
submitted without reviewing the generated media buys. Apostra prepares the
batch first, records that exact request as prepared, and then contacts sellers.

Use `mode: "stage"` when the media buys need review or customization:

1. Call `create_media_buys` with `mode: "stage"`.
2. Inspect the returned DRAFT media buys and update per-buy creatives, flight
   dates, pacing, or optimization goals as needed.
3. Call `execute_campaign` for the returned campaign. Alternatively, repeat
   `create_media_buys` with the same product query, campaign, selections, and
   `replace` value, changing only `mode` to `"execute"`.

The final execute submits those same DRAFT media buys. It does not reconcile
the product query again or discard the intervening changes. Repeating an
unchanged stage request is also a no-op. If preparation failed, the request is
not marked prepared and an unchanged retry attempts preparation again.

## Response

```json theme={null}
{
  "productQueryId": "query_123",
  "campaignId": "campaign_123",
  "campaignCreated": false,
  "mode": "execute",
  "campaignStatus": "ACTIVE",
  "queryProductsSelected": 4,
  "proposalsApplied": 1,
  "mediaBuysStaged": 0,
  "mediaBuysExecuted": 2,
  "mediaBuyRefs": [
    { "mediaBuyId": "mb_123", "status": "ACTIVE" },
    { "mediaBuyId": "mb_456", "status": "ACTIVE" }
  ],
  "mediaBuyRefsTruncated": false,
  "success": true,
  "noOp": false
}
```

External seller mutations cannot be atomic. `success: false` includes an
`errors` entry for each failed media buy; successful storefront buys remain
successful. Retry the unchanged request to dispatch only work that is still
DRAFT. The operation records a fingerprint on the campaign cart, so a completed
product query cannot be replayed with a different selection. `mediaBuyRefs`
returns lightweight IDs and statuses for up to 100 current buys; when
`mediaBuyRefsTruncated` is true, use `get_campaign` to drill into the cart.

### Amazon Sponsored Products Manual recovery

Amazon Sponsored Products Manual creation is currently authorized only for
explicitly enrolled internal test buyers. A nominated pilot, conditional
eligible-buyer widening, and GA are future stages with no committed date, not
current availability. Every external stage remains blocked
indefinitely after any customer `cleanup_required` row until durable
acknowledgement and one-business-day SLA proof are linked to that exact row, in
addition to every launch safety gate. This is not a public or all-market
availability promise. If access is turned off, new manual creates are blocked.
Account readiness, existing terminal receipts, retained-PAUSED containment,
cleanup, and internal test cleanup attestation remain available. This feature
adds no Apostra charge, plan, or entitlement.

Each Amazon Sponsored Products Manual selection must supply at least one ASIN
in `targetingOverlay.product_codes` and at least one entry in
`targetingOverlay.keyword_targets` (or select a product whose stored targeting
already supplies keywords). The adapter rejects a package missing either input
before creating a campaign.
Every Amazon create request must also resolve all packages to one authoritative
stored base product family. Mixed Amazon product families are rejected before
any provider write.

If an item returns `scope3_partial_creation`, **do not retry the create**. Amazon
created the identified Sponsored Products campaign, but it remains PAUSED
because required ad-group, product-ad, or keyword setup failed. The closed
`failure_stage` is `ad_group_create`, `product_ads_create`, or
`keywords_create`; a definitive pre-acceptance activation rejection uses
`campaign_activate`. `cleanup_required` is true and `retry_safe` is false.
Reconcile the exact authorized account and campaign identity in the error
with a full-buy cancel: call `PUT /campaigns/{campaignId}` (`updateCampaign`)
with `mediaBuys: [{ mediaBuyId: "<Apostra media-buy ID>", action: "cancel" }]`
and omit `packageIds`. Apostra binds that local media buy to the stored provider
account and exact campaign from the terminal identity before dispatch. This
cleanup remains available when the create flag is off. Read the resulting
status with `get_campaign`: `CANCELED` means Amazon confirmed cancellation;
`PAUSED` means Amazon retained the campaign and the exact cleanup marker remains
available for a later full-buy cleanup attempt. If cancellation itself returns
`scope3_mutation_uncertain` with `operation: cancel`, do not retry
automatically; the provider state is unconfirmed. The partial-create result
alone does not claim the campaign was canceled, deleted, or verified absent.

If an item returns `scope3_create_uncertain`, **do not retry the create**.
Amazon may have accepted the campaign, but no authoritative provider resource
identity was returned. The contract sets `cleanup_required: true` and
`retry_safe: false`. Reconcile the exact authorized account through the seller/
operator or support path. Do not invent a campaign identity or infer absence,
cancellation, deletion, or a safe retry from this result.

If an item returns `scope3_mutation_uncertain` with
`operation: create_activation`, **do not retry the create**. The exact campaign
exists, but activation may have crossed the provider boundary and bounded exact
readback did not confirm `state === enabled`. A PAUSED readback may be stale, so
the campaign state is unknown. `cleanup_required` is true and `retry_safe` is
false. Reconcile the exact account and campaign through the authorized seller/
operator using the same full-buy `updateCampaign` cancel shape above; omit
`packageIds`. The stored marker requires the exact provider account and campaign
before dispatch, and this cleanup remains available when the create flag is
off. Read the outcome with `get_campaign`: an Amazon-confirmed cancellation is
`CANCELED`; provider-confirmed containment remains `PAUSED` with the marker
available for another exact-account cleanup. A cancel-uncertain response is
terminal and must not be retried automatically.

Cleanup attestation is an internal test operational control, not a buyer API.
Buyers observe `CANCELED`, retained `PAUSED`, or outstanding terminal recovery
through `get_campaign`. Pilot, eligible-buyer, and GA widening remains blocked until
customer cleanup-required rows have a durable acknowledgement linked to the
exact row with one-business-day SLA proof. That hold is indefinite in this
release; availability is limited to explicitly enrolled internal test use.

## Errors

* `400 VALIDATION_ERROR` — unqualified IDs, an advertiser/campaign mismatch,
  a non-DRAFT cart, invalid campaign terms, a completed query replayed with
  different selections, or selections that cannot be priced/funded.
* `403 ACCESS_DENIED` — the caller cannot access the advertiser or campaign.
* `404 NOT_FOUND` — the product query, proposal, product, or campaign does not
  exist in the authenticated customer.
* `409 CONFLICT` — campaign budget, settlement, or seller state conflicts with
  the requested batch.

<Card title="Get products across storefronts" href="/v2/buyer/discovery/multi-storefront-get-products" icon="network-wired">
  Retrieve and refine the qualified proposals/products used by this operation
</Card>
