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

# Diagnosing a stuck media buy

> One call for buyers, three chat surfaces for sellers, and one place Scope3 can always trace a buy end to end.

"Why isn't my buy live?" used to mean campaign archaeology — cross-referencing status polls, guessing who was holding things up, and opening a support ticket with nothing to quote. It doesn't anymore. Every storefront-routed media buy carries a shared **`pendingReason`** vocabulary that says what it's waiting on and whose side owns the wait, a buyer-safe **`errorCode`** when something failed, and a **reference** each party can quote to the other. This guide walks the same buy from both sides.

* **Buyers** get there in one call — see [I'm a buyer](#im-a-buyer-why-isnt-my-buy-live).
* **Sellers** get three read-only surfaces in chat or REST — see [I'm a seller](#im-a-seller-what-needs-my-attention).
* Either side can escalate to Scope3 with a reference the other side already has — see [What Scope3 sees](#what-scope3-sees).

## I'm a buyer — why isn't my buy live?

### One-call lookup

`GET /api/v2/buyer/media-buys/{mediaBuyId}` (the `get_media_buy` operation, callable via `api_call`) returns one media buy with its full why-visibility field set. Use this first — it answers in one call instead of re-reading the campaign.

```bash theme={null}
curl https://api.interchange.io/api/v2/buyer/media-buys/mb_h3ctv001 \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```json theme={null}
{
  "mediaBuy": {
    "mediaBuyId": "mb_h3ctv001",
    "name": "CTV — Premium News Q3",
    "status": "PENDING_APPROVAL",
    "pendingAt": "salesagent",
    "pendingReason": "awaiting_source_moderation",
    "pendingSince": "2026-07-09T14:02:00Z",
    "sourceMessage": "Awaiting manual moderation",
    "forwardedAt": "2026-07-09T14:01:58Z",
    "buyerReference": "sf:17:sf_mb_example_9f2a1c",
    "startTime": "asap",
    "endTime": "2026-07-31T23:59:59Z",
    "createdAt": "2026-07-09T14:00:00Z",
    "updatedAt": "2026-07-09T14:02:01Z"
  }
}
```

(`mb_h3ctv001` and the `sf:` reference are illustrative — substitute your own media buy's id.) These fields also appear on the nested media buys in `GET /campaigns/:id` and on the media-buy status poll — this is just the fastest single-buy path to them.

### `pendingReason` — what it's waiting on, and whose side owns it

`pendingReason` is a **derived annotation, never a status** — the buy's `status` is unchanged; this just explains the wait.

| `pendingReason`                   | Whose side   | Meaning                                                                                                         | What to do                                                                                                            |
| --------------------------------- | ------------ | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `awaiting_storefront_approval`    | Seller       | The storefront operator has your buy in their review queue                                                      | Wait — no action needed unless the seller reaches out                                                                 |
| `awaiting_cancellation_approval`  | Seller       | You requested cancellation of a committed, guaranteed buy; the operator is deciding                             | Wait for the decision; the buy keeps delivering meanwhile                                                             |
| `awaiting_source_moderation`      | Seller       | The inventory source accepted the buy asynchronously and is still moderating it                                 | Wait; quote your `buyerReference` if the wait runs long                                                               |
| `no_creatives_attached`           | You          | The buy reached the source with no creatives attached — nothing is in review, so this never resolves on its own | Attach creatives to the buy (`update_media_buy` with `creative_ids`, or attach creatives to the campaign and re-sync) |
| `source_rejected_creatives`       | You          | Every creative assigned to the buy was rejected by at least one source agent                                    | Replace or fix the rejected creatives                                                                                 |
| `creative_processing_at_source`   | Seller       | The upstream buy exists; the source is processing or moderating its creatives                                   | Wait                                                                                                                  |
| `awaiting_creative_approval`      | Seller / You | The upstream buy exists, creatives are attached, and a review or assignment is genuinely pending                | Wait for the review; check the storefront's creative-approval queue if it runs long                                   |
| `forward_failed_retrying`         | Platform     | Forwarding to the source failed transiently; the platform is retrying                                           | No action — the platform retries automatically                                                                        |
| `forward_failed_needs_correction` | Platform     | Forwarding failed structurally; a correction is needed before it can retry                                      | Read `errorCode`, fix the flagged issue, resubmit                                                                     |
| `accepted_awaiting_trafficking`   | Seller       | Accepted end-to-end; the source hasn't started trafficking it yet                                               | Wait                                                                                                                  |
| `scheduled_not_started`           | Nobody       | Everything is booked; the flight start date hasn't arrived                                                      | Nothing to do — it launches automatically                                                                             |

See [Media buy lifecycle](/v2/concepts/media-buy-lifecycle#pendingreason-whose-side-owns-the-wait) for the full model, including how a buy's AdCP status, this annotation, and cancellation approval interact.

### `errorCode` — ownership and the seller's own words

When forwarding failed or the buy was rejected, `errorCode` carries one of a small buyer-safe set, paired with `errorOwner` (who owns the fix) and — when the source or reviewer provided one — their sanitized `sourceMessage`:

| `errorCode`                   | `errorOwner`  | Meaning                                                       | What to do                                                      |
| ----------------------------- | ------------- | ------------------------------------------------------------- | --------------------------------------------------------------- |
| `product_no_longer_available` | `platform`    | A selected product no longer resolves on the storefront       | Re-run discovery and rebuild the buy with current products      |
| `source_rejected`             | `seller`      | The inventory source declined the buy on its merits           | Read `sourceMessage`, address the reason, resubmit              |
| `storefront_rejected`         | `seller`      | The storefront operator rejected the buy in review            | Read `sourceMessage` (the reviewer's note), adjust, resubmit    |
| `source_unavailable`          | `seller`      | The source couldn't be reached or went silent — not a refusal | Retry later; escalate with your `buyerReference` if it persists |
| `invalid_request`             | `buyer_input` | The request needs a correction from you                       | Fix the flagged input and resubmit                              |
| `quote_expired`               | `platform`    | The held cross-currency rate expired before the buy forwarded | Re-discover and submit a new buy at the current rate            |
| `platform_error`              | `platform`    | A platform-side fault                                         | Contact support with your `buyerReference`                      |

`sourceMessage` is the first thing to read on a rejection — it's the source's or reviewer's own explanation, not a generic code.

### `forwardedAt` and `buyerReference` — what to quote to support

`forwardedAt` is when the buy was sent to its inventory source(s) — absent if it hasn't been forwarded yet. `buyerReference` (`sf:<storefrontId>:<mediaBuyId>`) identifies this buy's exchange with the seller. Quote it, together with `forwardedAt`, when contacting the seller or Scope3 support about a stuck buy.

### Transition notifications

The same facts push to your [notification stream](/v2/guides/notifications) as `media_buy.*` events, fired once per transition:

| Event                                  | Fired when                                                                                                          |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `media_buy.forward_failed`             | The first forward attempt failed (carries `errorCode`, and `pendingReason` when retrying or a correction is needed) |
| `media_buy.awaiting_source_moderation` | The source accepted the buy asynchronously and is moderating it                                                     |
| `media_buy.source_rejected`            | The source declined the buy (carries the sanitized reason)                                                          |
| `media_buy.stuck`                      | The buy never resolved at the source (timeout or task failure) and needs attention                                  |

Each payload carries `mediaBuyId`, `buyerReference`, and the applicable `pendingReason` / `errorCode` / `errorOwner` / `sourceMessage` — a reacting agent doesn't need a second call to learn why.

### Sample queries

<CodeGroup>
  ```bash Awaiting source moderation theme={null}
  curl https://api.interchange.io/api/v2/buyer/media-buys/mb_h3ctv001 \
    -H "Authorization: Bearer $SCOPE3_API_KEY"
  ```

  ```json Response theme={null}
  {
    "mediaBuy": {
      "mediaBuyId": "mb_h3ctv001",
      "name": "CTV — Premium News Q3",
      "status": "PENDING_APPROVAL",
      "pendingAt": "salesagent",
      "pendingReason": "awaiting_source_moderation",
      "pendingSince": "2026-07-09T14:02:00Z",
      "sourceMessage": "Awaiting manual moderation",
      "forwardedAt": "2026-07-09T14:01:58Z",
      "buyerReference": "sf:17:sf_mb_example_9f2a1c",
      "startTime": "asap",
      "endTime": "2026-07-31T23:59:59Z",
      "createdAt": "2026-07-09T14:00:00Z",
      "updatedAt": "2026-07-09T14:02:01Z"
    }
  }
  ```
</CodeGroup>

<CodeGroup>
  ```bash Forward failed, needs a correction theme={null}
  curl https://api.interchange.io/api/v2/buyer/media-buys/mb_h3ctv002 \
    -H "Authorization: Bearer $SCOPE3_API_KEY"
  ```

  ```json Response theme={null}
  {
    "mediaBuy": {
      "mediaBuyId": "mb_h3ctv002",
      "name": "Audio — Drive Time",
      "status": "PENDING_APPROVAL",
      "pendingAt": "storefront",
      "pendingReason": "forward_failed_needs_correction",
      "pendingSince": "2026-07-08T09:15:00Z",
      "errorCode": "invalid_request",
      "errorOwner": "buyer_input",
      "sourceMessage": "Flight end date is before flight start date",
      "forwardedAt": "2026-07-08T09:14:55Z",
      "buyerReference": "sf:17:sf_mb_example_7b310e",
      "startTime": "2026-07-15T00:00:00Z",
      "endTime": "2026-07-01T23:59:59Z",
      "createdAt": "2026-07-08T09:10:00Z",
      "updatedAt": "2026-07-08T09:15:01Z"
    }
  }
  ```
</CodeGroup>

<CodeGroup>
  ```bash Booked, waiting on flight start theme={null}
  curl https://api.interchange.io/api/v2/buyer/media-buys/mb_h3ctv003 \
    -H "Authorization: Bearer $SCOPE3_API_KEY"
  ```

  ```json Response theme={null}
  {
    "mediaBuy": {
      "mediaBuyId": "mb_h3ctv003",
      "name": "Display — Retail Homepage Takeover",
      "status": "PENDING_APPROVAL",
      "pendingAt": "salesagent",
      "pendingReason": "scheduled_not_started",
      "pendingSince": "2026-07-10T00:00:00Z",
      "forwardedAt": "2026-07-09T18:22:00Z",
      "buyerReference": "sf:17:sf_mb_example_2d44aa",
      "startTime": "2026-08-01T00:00:00Z",
      "endTime": "2026-08-31T23:59:59Z",
      "createdAt": "2026-07-09T18:20:00Z",
      "updatedAt": "2026-07-10T00:00:01Z"
    }
  }
  ```
</CodeGroup>

### Sample agent prompts

*"Check why media buy mb\_h3ctv001 isn't delivering and tell me whose side it's on."*

*"Use api\_call to get media buy mb\_h3ctv002 — what's the errorCode, who owns fixing it, and what does the source's message say?"*

*"Is my CTV buy on Premium News actually live yet? If not, tell me the pendingReason and when it started waiting."*

## I'm a seller — what needs my attention?

Three read-only surfaces answer "what do I have, where is one stuck, and what needs work" — each available as a REST endpoint and as an in-chat surface. Open them from the rail's **Operate** section, or just ask Murph.

### Media buys — every buy on the storefront

Ask *"show me all my media buys"* or *"show me everything that's up"*, or call `GET /media-buys`. It returns every buy — routed and ESA-managed — sorted by **urgency**: buys still waiting on someone whose flight starts within 48 hours (or has already started) come first.

<img src="https://mintcdn.com/agentic2/8jcbZrT7w04HUSkG/images/diagnosing-stuck-media-buys/01-media-buys-list.png?fit=max&auto=format&n=8jcbZrT7w04HUSkG&q=85&s=6e8462218cec19e598e60e702c6c1c4a" alt="Media buys list, urgency-sorted, showing a forward-failed buy, one awaiting source moderation, one pending approval, and completed buys" style={{ maxWidth: '420px' }} width="360" height="800" data-path="images/diagnosing-stuck-media-buys/01-media-buys-list.png" />

Each row carries the seller-facing `status` (a platform list-view convenience, never an AdCP status), the raw `sourceStatus`, the same `pendingReason` vocabulary buyers see, the `errorCode` of the latest failed exchange, and the `forwardOutcome`.

```bash theme={null}
curl "https://api.interchange.io/api/v2/storefront/media-buys?status=forward_failed" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```json theme={null}
{
  "items": [
    {
      "mediaBuyId": "sf_mb_example_7b310e",
      "kind": "routed",
      "buyer": { "customerId": 4821, "name": "Example Buyer Co" },
      "status": "forward_failed",
      "sourceStatus": null,
      "pendingReason": "forward_failed_needs_correction",
      "pendingSince": "2026-07-08T09:15:00Z",
      "errorCode": "invalid_request",
      "forwardOutcome": "failed",
      "flightStart": "2026-07-15T00:00:00Z",
      "flightEnd": "2026-08-31T23:59:59Z",
      "sourceCount": 0,
      "esaId": null,
      "createdAt": "2026-07-08T09:10:00Z",
      "forwardedAt": null
    }
  ],
  "total": 1,
  "warnings": []
}
```

### Media buy timeline — what was sent, when, and where it's stuck

Ask *"what did you send Meridian Ads for buy sf\_mb\_example\_9f2a1c and when?"*, or open the buy's timeline directly. `GET /media-buys/{mediaBuyId}/timeline` traces one buy end to end:

```
received → screened → decided → forwarded / forward-failed → submitted
        → source moderation (pending since T) → accepted / rejected → delivering
```

<img src="https://mintcdn.com/agentic2/8jcbZrT7w04HUSkG/images/diagnosing-stuck-media-buys/02-media-buy-timeline.png?fit=max&auto=format&n=8jcbZrT7w04HUSkG&q=85&s=41d969fdecb293202636f9a7023a5c11" alt="Media buy timeline showing a buy approved, forwarded, and parked in source moderation, with the source's own reference and the platform sf: reference" style={{ maxWidth: '420px' }} width="360" height="800" data-path="images/diagnosing-stuck-media-buys/02-media-buy-timeline.png" />

The forwarded payload is yours to inspect — you're the counterparty — minus platform-internal fields (webhook config and signing material are removed). A payload shown as `{"pruned": true}` means retention removed the stored bytes; the slim event record stays indefinitely.

**Whose reference to quote to whom:**

| Reference                                                       | Field                                                                                                      | Quote it to                                                                                           |
| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **Their reference** — the source's own ids                      | `theirReference.mediaBuyId` / `theirReference.taskId`                                                      | The inventory source — these are ids minted in *their* system                                         |
| **Platform reference** — the create idempotency key + timestamp | `platformReference.idempotencyKey` (`sf:<storefront>:<buy>:<source>`) with `platformReference.requestedAt` | The source (pre-acceptance) and Scope3 support — always quote the key **together with the timestamp** |

```bash theme={null}
curl https://api.interchange.io/api/v2/storefront/media-buys/sf_mb_example_9f2a1c/timeline \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```json theme={null}
{
  "mediaBuyId": "sf_mb_example_9f2a1c",
  "buyer": { "customerId": 4821, "name": "Example Buyer Co" },
  "approval": {
    "status": "approved",
    "submittedAt": "2026-07-09T13:58:00Z",
    "reviewedBy": "jordan@example-seller.com",
    "reviewedAt": "2026-07-09T14:00:30Z",
    "reviewerNotes": null,
    "forwardedAt": "2026-07-09T14:01:58Z",
    "submittedPayload": { "pruned": true }
  },
  "stages": [
    { "stage": "received", "at": "2026-07-09T13:58:00Z", "detail": null, "evidence": [] },
    { "stage": "decided", "at": "2026-07-09T14:00:30Z", "detail": "Approved by jordan@example-seller.com", "evidence": [] }
  ],
  "legs": [
    {
      "sourceId": "src_ctv_premium",
      "sourceName": "Premium News Network",
      "sourceKind": "adapter",
      "currentStatus": "pending_creatives",
      "forwardedAt": "2026-07-09T14:01:58Z",
      "responseReceivedAt": "2026-07-09T14:01:59Z",
      "theirReference": { "mediaBuyId": null, "taskId": "task_8f3c1a90-4d2e-4b77-9c1a-3e9b5d2f0a11" },
      "platformReference": { "idempotencyKey": "sf:17:sf_mb_example_9f2a1c:src_ctv_premium", "requestedAt": "2026-07-09T14:01:58Z" },
      "stages": [
        { "stage": "forwarded", "at": "2026-07-09T14:01:58Z", "detail": null, "evidence": [] },
        { "stage": "submitted", "at": "2026-07-09T14:01:59Z", "detail": "Awaiting manual moderation", "evidence": [] },
        { "stage": "source_moderation", "at": "2026-07-09T14:01:59Z", "detail": "Waiting since 2026-07-09T14:01:59Z", "evidence": [] }
      ],
      "forwardedPayload": { "pruned": true },
      "responsePayload": { "status": "submitted", "task_id": "task_8f3c1a90-4d2e-4b77-9c1a-3e9b5d2f0a11", "message": "Awaiting manual review by the source." },
      "payloadHighlights": { "flightStart": "asap", "flightEnd": "2026-07-31T23:59:59Z", "budget": 25000, "currency": "USD", "packageCount": 1, "productIds": ["ctv_premium_news"], "targetingKeys": ["geo", "device"] },
      "replayed": false,
      "note": null
    }
  ],
  "eventsTruncated": false
}
```

### Pending operations — everything waiting on someone

Ask *"show me everything pending"*, or call `GET /pending-operations`. It opens the portable Pending Operations Page and unions work in flight, grouped. A group appears only when it's non-empty: **approvals**, **creativeReviews**, **failedForwards** (grouped by error code), **awaitingSource**, **sourceDegradations**, and **gamCleanups**. Row actions open the exact Approvals, timeline, retry, or source-diagnostics surface without generating another chat prompt. A listed GAM cleanup uses an explicit two-step archive or manual-cleanup confirmation in the Page.

<img src="https://mintcdn.com/agentic2/8jcbZrT7w04HUSkG/images/diagnosing-stuck-media-buys/03-pending-operations.png?fit=max&auto=format&n=8jcbZrT7w04HUSkG&q=85&s=1d315f4fa91b6f3fc25037f9990ed7d5" alt="Pending operations view showing approvals waiting, a creative review, a structural failed-forward group offering only Escalate to Scope3, a transient failed-forward group offering Retry, and a buy waiting on source moderation" style={{ maxWidth: '420px' }} width="360" height="800" data-path="images/diagnosing-stuck-media-buys/03-pending-operations.png" />

### Recovery classes: why some failures have no retry

Every forwarding failure carries a recovery class, and the class — not optimism — decides which action is offered:

| Recovery class | Meaning                                                                                                                        | Action offered                                                                                                                              |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `transient`    | The send can succeed if repeated (source outage, timeout). The platform retries these itself for up to 6 hours after approval. | **Retry** — a one-decision confirmation. The create idempotency key makes a duplicate booking impossible if the source already received it. |
| `correctable`  | The request needs a change before any send can succeed.                                                                        | **Review correction** — opens the exact buy timeline; retrying the same payload would fail identically.                                     |
| `structural`   | No retry can ever succeed (e.g. the selected product no longer resolves). The platform terminalizes these.                     | **Review escalation** — opens the exact buy timeline and references. No retry button — one would silently do nothing.                       |

A `transient` failure whose retry window lapses is treated as terminalized and also becomes escalate-only.

### Status changed directly in your ad server

For an ad-server-backed (ESA-managed) source, your ad server holds the truth. If you approve an order directly in the ad server — say you approve a GAM order outside the storefront's approval flow — the storefront won't see that change until the source next syncs, so the buy can keep showing `pending_approval` while the order is actually approved and delivering. A source refresh reconciles it: it re-checks every non-terminal media buy (`pending_approval`, `pending_start`, `paused`) against the ad server and corrects any status that drifted. Ask Murph to *"refresh my \[source] source"* (or *"the GAM order is approved but the buy still shows pending — refresh it"*); Murph triggers the refresh and reports the reconciled status. You don't need to escalate for a status that the ad server has already resolved.

This does not cover a buy that already shows a terminal buyer-facing status like `REJECTED` — that's outside the refresh's non-terminal scope and needs a separate retry/reconciliation action, not just a source refresh.

```bash theme={null}
curl https://api.interchange.io/api/v2/storefront/pending-operations \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```json theme={null}
{
  "failedForwards": {
    "count": 1,
    "groups": [
      {
        "errorCode": "unknown_product_ids",
        "recovery": "structural",
        "action": "escalate",
        "count": 1,
        "items": [
          {
            "mediaBuyId": "sf_mb_example_7b310e",
            "buyer": { "customerId": 4821, "name": "Example Buyer Co" },
            "since": "2026-07-08T09:15:00Z",
            "terminalized": true,
            "action": "escalate",
            "references": {
              "idempotencyKey": "sf:17:sf_mb_example_7b310e:src_audio_drive",
              "requestedAt": "2026-07-08T09:15:00Z"
            }
          }
        ]
      }
    ]
  },
  "awaitingSource": {
    "count": 1,
    "items": [
      {
        "mediaBuyId": "sf_mb_example_9f2a1c",
        "sourceId": "src_ctv_premium",
        "sourceName": "Premium News Network",
        "buyer": { "customerId": 4821, "name": "Example Buyer Co" },
        "since": "2026-07-09T14:01:59Z",
        "taskId": "task_8f3c1a90-4d2e-4b77-9c1a-3e9b5d2f0a11"
      }
    ]
  }
}
```

You don't have to keep this surface open: a forward failure on an approved buy, and a source-moderation wait that ages past its deadline, each push one notification to your storefront's notification stream (in-app, email, Slack — per your delivery settings) — fired once, on the transition, never repeatedly.

### Sample agent prompts

*"Show me all buys that are up."*

*"Show me everything pending."*

*"What did you send Premium News Network for buy sf\_mb\_example\_9f2a1c, and when?"*

## What Scope3 sees

Every exchange between the platform and an inventory source is durably recorded, so support can trace any buy end to end from the same references you already have — no ticket that starts from scratch. When you escalate, quote your `buyerReference` (buyers) or the `sf:` idempotency key together with its request timestamp (sellers); that's the same handle Scope3 uses to pull up the exchange.

## Related

<CardGroup cols={2}>
  <Card title="Media buy lifecycle" href="/v2/concepts/media-buy-lifecycle" icon="diagram-project">
    The AdCP status enum, `pendingReason`, and how the three status surfaces relate
  </Card>

  <Card title="Media Buy" href="/v2/buyer/campaigns/media-buys" icon="receipt">
    The buyer media-buy object, including the full why-visibility field reference
  </Card>

  <Card title="Media buys & pending operations" href="/v2/storefront/media-buys/overview" icon="receipt">
    The seller-side REST reference for all three surfaces in this guide
  </Card>

  <Card title="Notifications" href="/v2/guides/notifications" icon="bell">
    Configuring where transition events are delivered
  </Card>
</CardGroup>
