Every budget is gross
Semicola’s standard terms take 0% of media: it charges for intelligent work in Intelligence Units, billed separately (see 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.
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:
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’sbudget.total. Campaign reads carry both numbers, so
read them rather than re-deriving them:
allocatedBudget: what the campaign’s buys hold. A buy that isDRAFT,PENDING_APPROVAL,INPUT_REQUIRED,ACTIVEorPAUSEDholds 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).
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 newbudget.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.
- Each entry names a buy on this campaign (
mediaBuyRefs) and packages of that buy (getwithkind: "media_buy"andinclude: ["packages"], orGET /api/v2/buyer/media-buys/{id}/packages). Per package you can changebudget(gross),pacingandbidPrice. An id that isn’t this campaign’s buy is404 NOT_FOUND; apackageIdthat isn’t that buy’s is400 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:
REST answers
202withproposals[](proposalId,mediaBuyId,status: "PENDING_SELLER_APPROVAL"), andsave_campaignreportsproposals. The campaign total is lowered at once, andunallocatedBudgetalready 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, each product’s share splits again by period.Currency
A campaign has onecurrency, 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).
- 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.”).
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 (AdCPfilters.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.
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 withFX_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.
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 withSPEND_DENOMINATION_UNRESOLVED(HTTP 422, don’t retry) and names the buys indetails.mediaBuyIds. Scope the request to exclude them to keep reporting on the rest. - A seller’s delivery answer states one
currencyonly when it covers every buy that reported money. Otherwise it omitscurrencyand each buy’stotals.spendand says why inerrors[]:MIXED_CURRENCY_DELIVERY(two currencies) orSPEND_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[]:cancelanddeleteactions,creative_ids,optimization_goals, per-buypacingPeriods, draftproducts[], and package flight dates or targeting through the campaign update (per-buy flight dates and targeting work throughupdate_media_buy).