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

# Budgets, fees and currency

> What a budget number means, how media buys allocate against the campaign budget, and how currencies must line up between a campaign and a seller.

## Every budget is gross

Semicola's standard terms take **0% of media**: it charges for intelligent work in Intelligence
Units, billed separately (see [Billing](/buy/billing)). Every budget you set is **gross**, the whole
amount, with any media fee carved out inside it rather than added on top. Under the standard terms
the fee is 0, so the gross budget is the media budget.

| Budget           | Where                                                       |
| ---------------- | ----------------------------------------------------------- |
| Campaign budget  | `budget.total` on the campaign, in the campaign `currency`. |
| Media buy budget | The budget staged against one seller.                       |
| Package budget   | Each product's share of a media buy.                        |

**Fee terms lock when a media buy is created.** The buy keeps its rate for its whole life: raising or
lowering its budget re-splits at that rate, and a later change to your terms only affects new buys.
Media buy reads carry `budget_denomination: "gross"` and a read-only `budget_breakdown`:

| Field                 | Meaning                                                             |
| --------------------- | ------------------------------------------------------------------- |
| `media_budget`        | The part of the gross budget that buys media.                       |
| `fee_amount`          | The fee carved out of it (0 under the standard terms).              |
| `fee_rate_percent`    | The fee as a share of media (`fee_amount ÷ media_budget × 100`).    |
| `effective_gross_cpm` | Gross budget ÷ impression goal × 1000; `null` when there's no goal. |

Buys created before fee terms were locked carry neither field. Sellers receive the media part only.

**Delivered spend is gross too.** Reporting (summary, daily, CSV), campaign lists and workspaces,
media buy pacing and the budget ceiling all state delivered spend at each buy's locked terms: a buy
that delivers in full shows spend equal to its gross budget. Buys created before the lock report spend
as the seller did.

## The budget ceiling

Media buys allocate against the campaign's `budget.total`. Campaign reads carry both numbers, so
read them rather than re-deriving them:

* **`allocatedBudget`**: what the campaign's buys hold. A buy that is `DRAFT`, `PENDING_APPROVAL`,
  `INPUT_REQUIRED`, `ACTIVE` or `PAUSED` holds its budget (a draft holds budget exactly as a live
  one does). A buy that has ended (`COMPLETED`, canceled, rejected or archived) counts what it
  actually delivered, gross, not its budget.
* **`unallocatedBudget`**: `budget.total − allocatedBudget`, the room left for a new buy or an
  increase. It can go negative; an over-allocated campaign is a launch blocker (see
  [Campaign readiness](/concepts/campaign-readiness)).

When you raise an existing buy, its current budget is already inside `allocatedBudget`. The most it
can go to is its current budget plus `unallocatedBudget`; adding the current budget again counts it
twice.

Staging a proposal with no room left fails with `409 INSUFFICIENT_MEDIA_BUDGET` ("No unallocated
budget left on … Raise the budget or remove a staged buy."). When a seller already has a draft buy on
the campaign, its budget counts as available to that seller's next proposal.

Lowering `budget.total` below what live buys (everything except drafts) have allocated fails with
`409 INSUFFICIENT_MEDIA_BUDGET` and `details.newTotal` / `details.committedAllocation` ("The new
budget total … is below the … live media buys have already allocated. Lower or cancel those media buys
first."). Lowering into the unallocated headroom works; staged drafts over the new total show as the
readiness warning instead. An executed campaign's budget changes through the campaign update like a
draft's; flight and targeting still change only on a draft.

A raise has the same ceiling from the other side: a package budget that would take the campaign's
buys, drafts included, past `budget.total` fails with `409 INSUFFICIENT_MEDIA_BUDGET` and
`details.budgetTotal` / `details.projectedAllocation` / `details.unallocatedBudget`, on the campaign
update and on `update_media_buy` alike. Package budgets are never cut for you.

### Lower a campaign and its buys together

To lower an executed campaign below what its live buys allocate, send the new `budget.total` and the
package reductions in **one** update: `mediaBuys[]` on `save_campaign` or
`PUT /api/v2/buyer/campaigns/{id}`. Don't lower the buys first and the campaign after.

```json theme={null}
{
  "expectedRevision": 7,
  "budget": { "total": 50000, "currency": "USD" },
  "mediaBuys": [
    {
      "action": "update",
      "mediaBuyId": "mb_…",
      "packages": [{ "packageId": "pkg_…", "budget": 12000 }],
      "updated_reason": "Reducing campaign budget"
    }
  ]
}
```

* Each entry names a buy on this campaign (`mediaBuyRefs`) and packages of that buy
  (`get` with `kind: "media_buy"` and `include: ["packages"]`, or
  `GET /api/v2/buyer/media-buys/{id}/packages`). Per package you can change `budget` (gross), `pacing` and `bidPrice`.
  An id that isn't this campaign's buy is `404 NOT_FOUND`; a `packageId` that isn't that buy's is
  `400 VALIDATION_ERROR`, before anything is sent to a seller.
* The request is checked against the allocation it would leave. If live buys would still allocate
  more than the new total, it fails with `409 INSUFFICIENT_MEDIA_BUDGET`, naming the new total and
  that projected committed allocation, and nothing changes.
* It applies atomically. Drafts change locally; live buys go to their sellers with the media portion
  of the new budget at the buy's locked fee terms, in the campaign currency (a cross-currency buy's
  seller converts it to its settlement currency at the buy's locked rate). If any seller refuses,
  nothing in the request is applied: the campaign total stays, sellers that already accepted are
  sent their previous values back, and the error names the buy (`field: "mediaBuys.<n>"`).
* A seller that must approve the change answers with an [update proposal](/buy/update-proposals):
  REST answers `202` with `proposals[]` (`proposalId`, `mediaBuyId`,
  `status: "PENDING_SELLER_APPROVAL"`), and `save_campaign` reports `proposals`. The campaign total
  is lowered at once, and `unallocatedBudget` already counts the pending reduction.
* One update per campaign at a time: a second one that arrives while the first is being applied gets
  `409 CONFLICT`; retry it shortly.

## How a budget splits across products

A staged buy splits its budget across the proposal's products by their allocation percentages. Shares
are exact to the cent: any leftover cents go to the products with the largest remainders, so the
packages always add up to the buy. With [pacing periods](/guides/pacing-periods), each product's share
splits again by period.

## Currency

A campaign has one `currency`, set when it's created. An update that sends a different
`budget.currency` fails with `CURRENCY_MISMATCH` ("This campaign buys in USD."). Every product in a
staged buy must be quoted in the campaign currency: natively, or converted from the seller's
settlement currency (see [Cross-currency buying](#cross-currency-buying)).

* A product quoted in another currency is **skipped** when you stage it (a proposal, a product
  selection or auto-select), with the reason "Priced in EUR; the campaign settles in USD." The
  result lists skipped products in `productsSkipped`.
* If no product in the proposal is priced in the campaign currency, staging fails ("None of this
  proposal's products can be staged.").

On the seller side, a storefront confirms the currency it quotes and settles in (**Confirm your
currency**, `currency_confirmed`, a go-live blocker) and may list additional settlement currencies
(`paymentCurrencies`). **Payout currency** (`settlement_currency_match`) checks that the default
currency is one of them. Nothing assumes USD: a storefront with no confirmed currency can't go live.

## Cross-currency discovery

Discovery asks each seller for prices in the campaign's currency (AdCP
`filters.pricing_currencies`). A Semicola storefront answers in one of three ways:

* **It settles in your currency.** Products are quoted as they are; no FX.
* **Your currency has a supported pair to its settlement currency.** Each price in the settlement
  currency is converted to yours at that pair's **rate-of-the-day** and the product carries
  `expiresAt` (REST) / `expires_at` (AdCP): the next UTC midnight. Until then the quote holds; after
  it, discover again for the new day's rate. The seller is still paid in its own currency and never
  sees yours.
* **Neither.** Discovery returns no products from that seller: "nothing for me here", not an error.
  A media buy in a currency the seller doesn't settle in is rejected.

Supported pairs are directed (buyer → settlement) and never a cross-product: **ZAR → USD** and
**IDR → USD** are supported; IDR → GBP and USD → IDR are not. A pair never makes its buyer currency a
settlement currency.

**Rates.** A pair is written `BASEQUOTE`: the settlement currency, then yours (`USDZAR` = rand per
dollar). Rates come from the ECB euro reference rates. The first value seen each UTC day is that
day's rate for everyone and doesn't move during the day. Converted prices are exact decimals rounded
half-even to the currency's minor units (none for IDR). If the feed is down when a new day's rate would
be fixed, the most recent locked rate (up to 7 days old) carries forward and operations are alerted;
past that the pair can't be priced and cross-currency requests fail with `FX_RATE_UNAVAILABLE`
(HTTP 503): retry later, or discover again once rates are flowing.

## Cross-currency buying

A campaign can buy from sellers that settle in other currencies; you always transact and are billed
in the campaign's currency.

* **One media buy per seller and settlement currency.** Staging splits a seller's products by the
  currency the seller is paid in. A ZAR campaign that picks USD-settled and ZAR-settled products from
  one seller gets two buys, both denominated in ZAR; each carries its own settlement currency and its
  own locked rate. Re-staging a seller's products replaces that settlement currency's draft only.
* **The rate locks when the buy is created.** Going live snapshots the rate-of-the-day onto each
  cross-currency buy. The quote you staged must be from the same UTC day (it hasn't passed its
  `expiresAt`); otherwise the launch fails with `FX_QUOTE_EXPIRED` (HTTP 409): run discovery again for
  a current quote, then resubmit. Every package, update, delivery report and payout of the buy then
  uses the locked rate. A retry keeps it, and a later rate move never changes a booked buy. A retry
  that sends different currency terms is refused ("This media\_buy\_id is already bound to a different
  immutable FX rate."): resubmit with the original terms or as a new media buy. A buy's currency never
  changes.
* **The seller is paid in its own currency.** The storefront converts package budgets and bids back
  to its settlement currency at the locked rate before forwarding to its inventory source; the source
  never sees your currency. Bids are checked against the source's floor converted at the same rate.
* **Manual-approval sellers** hold the rate quoted at submission and re-apply it when they approve.
  The hold lasts 72 hours; a buy approved after that isn't booked at the stale rate and needs a fresh
  quote.

**Delivery and reporting.** Sources report spend in the currency they're paid in, and Semicola
converts it once, into your currency, at the buy's locked rate (then states it gross at the buy's fee
terms). Reporting names the conversion on each converted media buy (`deliveryFxConversion`:
`fromCurrency`, `rate`, `asOfDate`, `source: "booked"`; MCP `get_delivery` lists the same per buy in
`deliveryFxConversions`) and the reporting table shows it under the buy. Only buys that are really
cross-currency are converted:

* Delivery a source reports in a currency it isn't paid in is never converted. Time-series rows keep
  the currency they were reported in (every row carries `currency`); the summary fails with
  `SPEND_DENOMINATION_UNRESOLVED` (HTTP 422, don't retry) and names the buys in `details.mediaBuyIds`.
  Scope the request to exclude them to keep reporting on the rest.
* A seller's delivery answer states one `currency` only when it covers every buy that reported money.
  Otherwise it omits `currency` and each buy's `totals.spend` and says why in `errors[]`:
  `MIXED_CURRENCY_DELIVERY` (two currencies) or `SPEND_DENOMINATION_UNRESOLVED` (spend with no
  currency). Impressions and other counts are unaffected.

## Not available yet

* **Per-source execution currency** for ad-server sources.
* **The rest of update-campaign `mediaBuys[]`**: `cancel` and `delete` actions, `creative_ids`,
  `optimization_goals`, per-buy `pacingPeriods`, draft `products[]`, and package flight dates or
  targeting through the campaign update (per-buy flight dates and targeting work through
  `update_media_buy`).
