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

# When a campaign cancel stalls

> How a campaign that is ending names the media buys whose cancel stalled, what each state means, and what to do next.

Canceling a campaign cancels every media buy under it, and each seller answers
on its own time. The campaign reads as `phase: "ending"` until every buy has
ended. Most of the time that is only waiting. Sometimes a buy's cancel did not
reach its seller, or the seller refused it, and the campaign stays `ending`
until someone sends the cancel again.

While the phase is `ending`, reading the campaign returns `stalledCancels`: the
buys that will not finish ending without a new request, each with the reason.

```json theme={null}
{ "kind": "campaign", "id": "cmp_123" }
```

```json theme={null}
{
  "stalledCancels": [
    {
      "state": "refused",
      "mediaBuyId": "mb_1",
      "since": "2026-11-11T09:30:00.000Z",
      "error": { "code": "NOT_CANCELLABLE", "recovery": "terminal" }
    },
    {
      "state": "not_delivered",
      "mediaBuyId": "mb_2",
      "since": "2026-11-11T09:30:04.000Z"
    },
    { "state": "not_sent", "mediaBuyId": "mb_3" }
  ]
}
```

The response text names each buy and its state too.

## An empty list

An empty `stalledCancels` means every open buy is waiting normally: ending at
its seller, waiting for an operator to approve the cancel, or being sent or
retried by Apostra. There is nothing to do but wait. The list is a statement
about the buys that need a new request, so it is not a progress report.

## The three states

| State | What it means | What to do |
| - | - | - |
| `refused` | The seller answered the cancel with an error and did not apply it. A cancel the seller's operator rejected when it was filed for approval reads `refused` too, with no `error`. `since` is when the answer was recorded. | Read `error.recovery`. `correctable` or `terminal`: ask the seller first, for example about its cancellation policy, because sending the same cancel again will not help. `transient`: try again later. No `recovery`: ask the seller. |
| `not_delivered` | Apostra tried to send the cancel and gave up without a seller answer: a transport failure, a timeout, an internal failure, or retries that ran out. An approved cancel that Apostra could not forward to the seller reads this way too. The seller refused nothing. | Send the cancel again. Do not contact the seller. |
| `not_sent` | The buy is still open and Apostra holds a complete record that no cancel was sent for it, for example because the request ended before it reached this buy. | Send the cancel now. |

To send it again, use `save_campaign` with `desiredPhase: "canceled"` and the
campaign's current `expectedRevision`. That call sends the cancel only for the
buys that are pending or failed, and never cancels a confirmed buy twice. To
cancel one buy, use `save_media_buy` with `mediaBuyId` and `isCanceled: true`.

## What `error` holds

`error` uses AdCP's own error shape, with only two fields:

* `code` is present only when it is one of AdCP's standard error codes. A
  seller may send a code of its own; it is left out.
* `recovery` is present when the seller sent `transient`, `correctable` or
  `terminal`.

`error` is absent when the seller sent neither a standard code nor a
`recovery`, and for a rejected approval. The state, not `error`, says the
seller refused. The seller's own `message`, `suggestion`, `details` and
`buyer_reason` are never returned.

## When the list is absent

The field is absent in every phase except `ending`. It is also absent for a
campaign whose cancel was requested before Apostra began recording each cancel
it sends: for those, a buy with no cancel on record does not prove that none
was sent. Such a campaign reads as it did before, with the phase `ending` and
no list. Send the cancel again if it has stayed `ending`.

Related: [cancel a campaign and its media buys](https://docs.apostra.com/v3/tool-reference.md), and the
[limits of v3 cancellation](https://docs.apostra.com/v3/limitations.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.