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

# Media-buy approvals

> Review the queue of pending media buys and approve or reject them before they reach inventory

When a buyer submits a media buy to your storefront, it lands in your approval queue as a `pending` entry holding the raw AdCP `create_media_buy` payload. You inspect it and record a decision: `approved` or `rejected`. Approved entries are forwarded upstream to the underlying sales agent by the storefront's MCP layer; the `forwardedAt` stamp tells you when that succeeded.

For when this queue is used — the `mediaBuyApproval` setting, the manual-review
posture, and how you find out something is waiting — see
[Reviewing buyer transactions](/v2/storefront/approvals/overview).

## What the pre-screen verdict means

Each queued entry carries one of three pre-screen verdicts: *clearly on policy*,
*needs a look*, or *clearly against an explicit rule*. Deterministic hard
findings supply a strong operator-facing rejection signal; AI-judged conflicts
can only escalate. For the full verdict model, the one-way AI second opinion,
and how the agent reads your written policy, see
[AI Business Rules](/v2/concepts/ai-business-rules).

## Delivery readiness

The pre-screen verdict answers "may I sell this?" It does not answer "can this
buy actually run?" A buy can be perfectly on policy and still be unable to
deliver — most often because no creative reached its packages. Each pending
`create` entry therefore also carries a `readiness` block, computed from the
buyer's live media buy rather than the payload frozen at submit time:

* **`packages[]`** — one entry per package as it will actually be forwarded.
  `assignedCreativeIds` empty means that package will be sent with no creative,
  which the destination will usually refuse. Capped at 20 entries with
  creative-less packages listed first, so the cap can never be what hides one;
  `packageCount` is the buy's true package total and
  `creativelessPackageCount` counts the unfilled packages across all of them.
* **`unusableCreatives[]`** — for a package with no creative, each creative on
  the buyer's campaign that could not satisfy it and why: the measured value
  against the seller's declared requirement (*"requires a duration of at least
  30000ms but asset is 15047ms"*), or an ambiguous format identity naming the
  candidate options the buyer has to choose between. An empty list alongside an
  empty `assignedCreativeIds` means the buyer's campaign has no creatives at
  all. The list is capped at five entries; `unusableCreativeCount` carries the
  true total, so a truncated list is never mistaken for the whole answer.
* **`compatibleUnattachedCreativeIds[]`** — creatives on the campaign that *do*
  fit the package and still never attached to it. This is our failure, not the
  buyer's: there is nothing for them to change. Report it rather than rejecting
  the buy. Kept separate so a package that demonstrably fits is never described
  as one the campaign has no creative for.
* **`creativeSyncFailures[]`** — creatives on that campaign whose sync to your
  storefront failed, with whatever reason the sync recorded. This reflects each
  creative's latest sync state, so a failure the buyer has since fixed drops
  off. Capped at 10 entries; `creativeSyncFailureCount` carries the total.

**Readiness is disclosure, not a gate.** An unready buy still forwards when you
approve it — a media buy never fails to forward on creative grounds, because a
creative can be attached after booking. What readiness changes is that you see
the state *before* spending a decision, and can go back to the buyer instead.

`readiness` is `null` on decided entries, on `update` approvals, when the check
could not run, and past the first 50 entries of a very long queue. Null always
means "not computed" — never "checked and healthy".

An entry moves through `pending → approved` or `pending → rejected`. Decisions are terminal — deciding an entry that is no longer `pending` is rejected, so a double-decide cannot slip through. An entry becomes `revoked` when the buyer cancels or deletes the media buy before it reaches a source: a cancellation while the entry is still `pending` withdraws it and closes its approval task, and an approved entry whose buy the buyer has already canceled is revoked instead of forwarded. A `revoked` entry is never forwarded, retried, or decided, and it is not something you set on the decide endpoint. If you approve an entry before the buyer's cancellation arrives, your approval stands and the buyer cancels the buy once it reaches its source. Each entry is keyed by the buyer's AdCP `mediaBuyId`.

The portable Approvals Page keeps the decision and its forwarding result
separate. If your approval is recorded but delivery to one or more sources is
incomplete, it shows a partial-success notice and lets you explicitly confirm a
typed retry without leaving the Page. The same idempotency key is reused, so a
source that already received the buy cannot create a duplicate booking. A
source response that means "accepted and still processing" does not offer a
duplicate retry.

A forward a source refuses structurally is usually not a dead end. The entry is
stamped `forwardedAt` and the platform stops re-attempting it — re-sending an
identical payload to a source that refused it would fail the same way every
time. Once the underlying cause is fixed, the API can recover the buy in place:
[Retry forwarding](/v2/storefront/media-buy-approvals/tasks/retry-forward) with
`forceTerminal` re-sends it under the same `mediaBuyId` with your approval
intact, so the buyer does not need to resubmit under a new id. This is an API
control; the chat and Pending operations surfaces still offer escalation rather
than a retry for these buys. A buy terminalized *before* any source was
contacted because of an unconfigured settlement currency, an expired FX quote,
or another permanent prerequisite cannot be recovered this way and must be
resubmitted. A storefront intake hold is different: the approved buy remains
queued with `forwardedAt: null` and forwards automatically after intake resumes.
Time spent on hold does not use up the forwarding retry window, and the original
approval time remains unchanged.

## Buyer task callbacks

Buyer agents can attach AdCP push-notification config to the original `create_media_buy` call. The storefront stores that callback configuration separately from the operator-facing payload and uses it when the approval resolves.

Storefronts now use the SDK 14 compatibility projection for direct AdCP
responses. Custom capability metadata is canonical at
`ext.scope3.storefront`; the deprecated `extensions.scope3.storefront` path is
temporarily dual-emitted in both MCP response representations while AdCP 3.0
and 3.1 buyers migrate. Demo disclosures also live below the canonical `ext`
path. See the [migration guide](/v2/setup/migration-guide) for the complete
format, pricing, task-protocol, and response-field mapping.

```json theme={null}
{
  "media_buy_id": "mb_2026_q2_ctv",
  "packages": [
    {
      "product_id": "run-of-site",
      "budget": 50000
    }
  ],
  "push_notification_config": {
    "url": "https://buyer.example.com/adcp/tasks",
    "token": "buyer-echo-token",
    "authentication": {
      "schemes": ["HMAC-SHA256"],
      "credentials": "shared-webhook-secret"
    }
  }
}
```

Use `push_notification_config` for canonical AdCP requests. The storefront also accepts `pushNotificationConfig` from clients that pass camelCase JSON through a REST/MCP proxy. Callback URLs must be public HTTPS endpoints; localhost, private-network, link-local, metadata, and internal hostnames are rejected. Supported authentication schemes are `Bearer` and `HMAC-SHA256`.

When the operator approves and every underlying source accepts the forwarded buy, the buyer webhook receives a signed ADCP task event with `status: "completed"` and a `result` containing the accepted `media_buy_id`. When the operator rejects, or approval succeeds but source forwarding fails, the webhook receives `status: "failed"` with an AdCP-compatible error object. Polling the submitted task remains the fallback if no callback is supplied or delivery fails.

Media-buy task events use the AdCP task protocol `media-buy`; creative approval task events use `creative`. These webhook protocol values are distinct from the snake-case `media_buy` value advertised by storefront capabilities.

<Note>
  The approval row returned to storefront operators omits `push_notification_config` and `pushNotificationConfig` from `submittedPayload` so webhook credentials are not exposed in the queue UI or REST responses.
</Note>

All examples use the storefront base URL:

```
https://api.apostra.com/api/v2/storefront
```

Authenticate every request with `Authorization: Bearer $SCOPE3_API_KEY`.

## Task reference

<CardGroup cols={2}>
  <Card title="List approvals" href="/v2/storefront/media-buy-approvals/tasks/list-approvals" icon="list">
    `GET /media-buy-approvals` — the approval queue, newest first
  </Card>

  <Card title="Get an approval" href="/v2/storefront/media-buy-approvals/tasks/get-approval" icon="magnifying-glass">
    `GET /media-buy-approvals/{mediaBuyId}` — one queue entry
  </Card>

  <Card title="Decide an approval" href="/v2/storefront/media-buy-approvals/tasks/decide-approval" icon="gavel">
    `POST /media-buy-approvals/{mediaBuyId}/decide` — approve or reject
  </Card>

  <Card title="Retry forwarding" href="/v2/storefront/media-buy-approvals/tasks/retry-forward" icon="rotate-right">
    `POST /media-buy-approvals/{mediaBuyId}/retry-forward` — re-send an approved buy
  </Card>
</CardGroup>
