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

# Update proposals

> Poll and cancel a pending change to a live media buy while its seller decides.

When you change a live media buy (budget, pacing, bid, targeting overlay or flight end) with
`update_media_buy`, Semicola sends the change to the seller. If the seller applies it at once, the buy
updates and you're done. If the seller has to approve it first, the change becomes an **update
proposal**: a pending change that moves from `PENDING` to `APPROVED` or `REJECTED`.

While a proposal is `PENDING` you can poll it or cancel it. Once it's resolved, it's final.

## How a proposal starts

`update_media_buy` (or `PATCH /api/v2/buyer/media-buys/{id}`) returns the buy and an `updateProposal`:

```json theme={null}
{
  "mediaBuy": {
    "id": "…",
    "status": "ACTIVE",
    "pendingChange": {
      "status": "PENDING_APPROVAL",
      "reason": "Shift budget to the stronger package",
      "proposalId": "…",
      "differences": { "budget": { "live": 5000, "proposed": 7500 } }
    }
  },
  "updateProposal": { "id": "…", "status": "PENDING", "mediaBuyId": "…" }
}
```

`updateProposal` is `null` when the seller applied the change straight away. While a proposal is
pending, the buy carries a `pendingChange` listing only the fields whose pending value differs from the
live one (`budget`, `endTime`), so you can show "live" and "proposed" side by side.

**One pending change per buy.** A second `update_media_buy` on the same buy is refused with `CONFLICT`
("… already has a change waiting for the seller's approval. Cancel update proposal … or wait for the
seller to decide.") until the first is resolved or cancelled.

## Poll a proposal

| MCP                                  | REST                                      |
| ------------------------------------ | ----------------------------------------- |
| `get_update_proposal` (`proposalId`) | `GET /api/v2/buyer/update-proposals/{id}` |

```json theme={null}
{ "id": "…", "status": "APPROVED", "mediaBuyId": "…", "resolvedAt": "2026-11-02T09:14:00.000Z" }
```

Each poll of a `PENDING` proposal asks the seller for the status of its task. If the seller is
unreachable, the proposal stays `PENDING` and the next poll asks again.

| Status     | Meaning                                                                                           |
| ---------- | ------------------------------------------------------------------------------------------------- |
| `PENDING`  | Waiting on the seller.                                                                            |
| `APPROVED` | The seller accepted; the change is applied to the buy. `resolvedAt` is set.                       |
| `REJECTED` | Not applied. `rejectionReason` says why: the seller's message, `cancelled_by_buyer` or `expired`. |

A proposal the seller hasn't decided within **72 hours** expires: it becomes `REJECTED` with
`rejectionReason: "expired"`, and you get a notification.

## Cancel a proposal

| MCP                                                             | REST                                         |
| --------------------------------------------------------------- | -------------------------------------------- |
| `cancel_update_proposal` (`proposalId`; Semi confirms with you) | `DELETE /api/v2/buyer/update-proposals/{id}` |

Cancelling resolves the proposal as `REJECTED` with `rejectionReason: "cancelled_by_buyer"` and frees
the buy for a corrected update. It **doesn't recall** a request already sent to the seller. If the
seller's decision lands first, the seller's decision wins and cancel returns the resolved proposal.
Cancelling a proposal that's already resolved returns `CONFLICT`.

Cancellations show in [Activity](/guides/audit-logs) as a media buy update.

## Related

* [Targeting](/buy/targeting)
* [Media buys and packages](/buy/media-buys-and-packages)
* [Diagnosing a stuck media buy](/guides/diagnosing-stuck-media-buys)
