This is a Seller Account operator capability, and it’s opt-in per source: it
only applies to sources you’ve uploaded a feed for (your first committed upload
opts the source in automatically; you can also enable it explicitly). Use it
when any product on the source needs uploaded fallback pricing; the source can
still use ad-server pricing for its other products.
Which ad servers need this
Automatic wholesale pricing is derived from the ad server’s delivery reporting, and today only Google Ad Manager supports that. What the module means for your source depends on the ad server behind it:
For ad servers without automatic pricing, this is not a failure state: the
source is not broken, it simply prices via manual upload until that ad server’s
pricing sync ships. Inventory sync, trafficking, delivery, and reporting are
unaffected.
How Google Ad Manager pricing updates work
For a Google Ad Manager source, Apostra reads your GAM delivery and derives price guidance from its delivery percentiles. It applies that guidance, and an auction floor, to auction-priced options only. A fixed price you set is never changed by the sync. Apostra does not add price guidance, an auction floor, or a minimum spend to a fixed option. To sell from your own rate card, give each product fixed pricing options only. Pricing options are replaced as a complete list when you save a product. If you remove an auction option, it is removed for good: the GAM sync does not add pricing options back. A product can keep both fixed and auction options. Buyers may be offered either option, and Apostra uses the higher-priced option.Selector ids and types by ad server
Each feed row is matched to your products by its selector type + selector id, compared against the selectors your products are keyed on. Use exactly the ids and types shown in the selector browser (or on the product itself) — each ad server has its own selector vocabulary:Filter the selector browser by media and environment
When a source contains both display and video inventory, narrow the selector browser before copying ids into the feed.browse_ad_server_selectors accepts:
The REST selector endpoint accepts the same filters as
mediaType and
environmentType (or their snake-case aliases media_type and
environment_type). A value outside these lists returns 400 instead of
silently broadening the search. These filters help you find the right
selector; they do not replace the format reference used to keep pricing rows
separate.
What you supply
One row per ad-server selector × country. Column order and header capitalization don’t matter; CSV and XLSX both work.Download your pre-filled template
GET /inventory-sources/{sourceId}/wholesale-pricing/template returns a CSV
with one row per ad-server selector your products already reference —
selector ids, names, your settlement currency, and an effective window are
filled in. You add only the CPM percentiles and monthly impressions.Required columns
Optional columns
Other columns are normally preserved as metadata and ignored by validation.
Device-dimension aliases are the exception and are rejected as described below.
Format-specific pricing behind a shared selector
If display and video products share an ad-server selector, add a publisher-scopedformat_option_ref to keep their prices separate. The value
is JSON stored in the CSV or XLSX cell:
publisher_domain and format_option_id must exactly match the product’s
catalog declaration. Copy the format option id from the product rather than
inventing a label. Product-scoped references are not supported by this
component-level feed.
For example, two products can use the same selector and country while keeping
different prices:
Use one consistent shape for each selector: either every row has a
format_option_ref, or none does. A scoped/unscoped mix is rejected even when
the selector currently matches only one product. Scoped rows that resolve to
the same product and country must not have overlapping effective windows;
adjacent windows are allowed, so one row may end on the date the next begins.
Device is not a supported pricing dimension. Columns such as device_type,
deviceType, or Device Type are rejected rather than treated as product
identity. Model device-specific inventory as distinct catalog products with
their own selectors before pricing it.
Preview can return these format-scope diagnostics:
SDK 14 does not change this feed’s
format_option_ref authoring contract.
Buyer-facing AdCP 3.1 products expose the matching canonical
format_options[]; negotiated AdCP 3.0 responses expose the required projected
format_ids[] legacy references instead. Buyers should use the format reference
returned by their negotiated protocol version and supply the selected
format_id when syncing creatives. This feed remains CPM-only; the
revenue_share pricing model available in broader media-buy reporting does not
apply to uploaded wholesale rows.GLOBAL or per-country, not both
AGLOBAL row prices a selector’s whole inventory; a country row prices one
market’s slice of it. Supplying both for the same selector double-counts
avails — the platform cannot tell whether the country row is a subset of the
GLOBAL row or additional inventory on top of it — so a selector must use
either one GLOBAL row or only country rows, never both. A file that mixes
them is rejected: every row for that selector is returned as a row-level
GLOBAL_COUNTRY_CONFLICT error at preview, and the feed cannot be committed
until you pick one shape and remove the other rows.
The pre-filled template starts every selector as a single GLOBAL row. If you
price a selector by country, delete its GLOBAL row and add one row per
country instead.
Selectors that don’t match any product
Every preview and commit response carries aselectorDiffAvailable flag
telling you whether the feed’s selectors could be checked against the
source’s current products:
selectorDiffAvailable: true— the response also reportsmatchedSelectorCount,unmatchedSelectorCount, andunmatchedSelectors(the first 5 unmatched selectors, named; the count carries the full total). A row can pass every validation rule and still price nothing if itsad_server_selectororselector_typedoesn’t match what your products actually reference — a typo, the wrong selector type (see the warning above), or a selector that belongs to a different source. A committed feed whose selectors match zero products still succeeds (nothing in the file was invalid), but the unmatched fields name exactly which selectors priced nothing, and readiness will not report pricing as current for products that still need a feed — see Readiness and freshness. If you see unmatched selectors after a commit, re-check the ids againstbrowse_ad_server_selectorsor the pre-filled template.selectorDiffAvailable: false— the source’s products haven’t been synced yet (or the catalog couldn’t be read), so there was nothing to compare against. The match fields are omitted andselectorDiffNoteexplains why. This is not a mismatch: uploading pricing before the first product sync is fine, and the check will run on your next preview/commit once products exist.
A wholesale feed is single-currency
Apostra never converts currency for you — there is no FX. A wholesale feed prices one selector’s inventory in exactly one currency, and that currency must be your Seller Account’s confirmed settlement currency (the same currency pre-filled on the template).- A row priced in any other currency is rejected, individually, at preview and commit — it never silently reaches buyers in the wrong currency. The rejection names the row and the mismatched currency.
- A feed cannot mix currencies. If your Seller Account hasn’t confirmed a settlement currency yet, every row must still share one currency; a file with more than one is rejected. Once your Seller Account confirms its settlement currency, every row must match it.
- This also applies to a fixed price set directly on a product (
pricing options[].is_fixed, see Readiness and freshness): a fixed price in the wrong currency is not a complete price on this Seller Account and never makes the product buyable, even though the price itself is otherwise valid.
How pricing is used
You give the platform a price distribution per country, not a single number:- The floor is derived from
p25— the platform will not sell below your 25th-percentile clearing price. Wherep25is0(run-of-network display that clears at $0 part of the time), the floor is0and the product sells at open auction, guided byp50/p75. p50andp75are guidance for pricing above the floor. The platform does not anchor the sell price on a percentile — the buyer’s agent chooses a price within your distribution.- Your per-country prices and availability stay per-country. Your selectors map to the buyer-facing products (a product can span several selectors), and each product carries a per-country forecast: the available impressions and the expected CPM range for each country you priced. A buyer targeting the US sees your US price and avails; one targeting the UK sees the UK’s — without splitting your catalog into a separate product per country.
Example feed
Copy this template into a CSV file. The columns are identical for every ad server — only thead_server_selector ids and selector_type values differ.
For a Google Ad Manager source:
selector_type):
Uploading
Upload from the ad-server source page
Open Ad server source, choose the source, and use Import seller feed in the Wholesale price & avails section. This opens the shared Import inventory feed Task. Download the pre-filled template, upload or paste the completed rows, review accepted and rejected rows plus selector matches, and then commit. The Task keeps preview and commit scoped to the same source; any file edit or source change invalidates the preview. The same Task also handles modular static-avails feeds when it is launched with that module contract. It is not a general pricing editor: Rate card documents, Playbook selling guidance, and buyer discounts remain in their owning surfaces.Upload from Apostra chat
Attach the completed CSV or XLSX feed in Apostra and identify it as a wholesale pricing upload for the inventory source. Apostra first runs the same deterministic preview as the API and reports the accepted and rejected row counts plus every row-level rejection, along with which selectors matched or didn’t match a product on the source. After the feed has zero rejected rows, ask Apostra to commit it and use the Approve control. Nothing is written before approval. Apostra checks the selector-match fields on the commit response too, so a feed that priced nothing still gets flagged even though the commit itself succeeded — and when the source’s products aren’t synced yet, Apostra reports the match check as unavailable rather than claiming a mismatch. The approved upload fully replaces the source’s prior feed. Check that the file contains every selector you still intend to price; an omitted selector loses its uploaded fallback. Use this flow only for the wholesale pricing template. Rate cards and buyer discounts use their respective setup flows.Upload through the API
All examples use the legacy v2storefront base URL for Seller Accounts:
0. Download a pre-filled template
Skip hand-building the file: the template endpoint returns a CSV with one row per ad-server selector referenced by the source’s products, pre-filled with everything the platform already knows (selector id, type and name,GLOBAL
country, your confirmed settlement currency, cpm, a quarter-long effective window,
and a dated source_version). Fill in p25_cpm/p50_cpm/p75_cpm and
monthly_impressions, then upload it back. Add per-country rows by duplicating
a selector’s row with a specific country if you price markets separately.
1. Enable the module on your source (optional)
Your first committed upload attaches the module automatically. Call this explicitly only if you want to set the minimum line-item spend up front:2. Preview
Submit rows (or paste CSV) withoutconfirmed to see exactly what will be
accepted or rejected before anything is written:
acceptedRowCount, rejectedRowCount, and a per-row
rejected list — each with the row, the column, and why (for example, a
missing p50_cpm, a fake currency code, a currency that doesn’t match your
Seller Account’s settlement currency (or another row in the same file), a p25 > p50 ordering error, an implausible outlier, a non-ISO date, or a selector
mixing a GLOBAL row with a country row). It also carries the
selector-match fields (selectorDiffAvailable and, when available,
matchedSelectorCount / unmatchedSelectorCount / unmatchedSelectors) —
see
Selectors that don’t match any product.
3. Commit
When the preview is clean, re-submit withconfirmed: true. A commit is
rejected if any row is still rejected — fix and re-preview first. Each commit
fully replaces the prior feed for the source. A selector you drop loses its
uploaded fallback, but products with eligible ad-server pricing remain sellable.
A successful commit refreshes the source’s Seller Account products automatically;
no separate catalog refresh is required.
The response’s catalogRefresh fields report whether cache invalidation and the
immediate warm completed. A refresh failure does not roll back an accepted feed.
The commit response also repeats the selector-match fields — a commit can
succeed with every row valid and still price nothing if its selectors don’t
match a product; check unmatchedSelectorCount (when
selectorDiffAvailable is true) rather than treating a 200 as proof the
feed did something.
.xlsx or .csv file can be uploaded directly to
POST /inventory-sources/$SOURCE_ID/wholesale-pricing/upload as multipart
form-data (field file, plus confirmed=true to commit).
Readiness and freshness
Pricing coverage is evaluated product by product. On a Google Ad Manager source, Apostra automatically uses non-guaranteed pricing derived from the ad server’s priority, network, and bulk-priority demand tiers. House demand has no commercial price, while sponsorship and standard demand are guaranteed and do not produce non-guaranteed CPM guidance. On SpringServe, FreeWheel, and AdsWizz sources there is no automatic derivation yet, so every product’s pricing comes from your uploaded feed. Products with current ad-server pricing keep that pricing. An uploaded file is used only for products where the ad server could not produce eligible pricing; you do not need to replace the whole source’s price catalog. Ad-server and uploaded pricing are guidance, not a condition for selling. For an auction product (no fixed rate) they become the floor and price guidance a buyer sees, and they are never required: a product with an auction pricing option in your settlement currency stays listed whether or not the ad server could price it and whether or not your uploaded feed is current. With no guidance, buyers see the auction option with no floor and no price guidance, and decide for themselves whether to bid. Booking such a product still needs a positivebid_price from the buyer.
A fixed price is different: it is a buy-it-now price, and it comes from you.
Set it directly on the product — a pricing option with is_fixed: true, a
positive rate, and a currency matching your Seller Account’s settlement
currency is a complete price on its own and needs neither reporting history
nor a feed. A fixed price in any other currency is not a complete price here —
there is no FX — and a product whose only price is that fixed price stays
hidden until the price is set in your settlement currency (the authoring
surface warns you when this happens). A product with no pricing option at all
also stays hidden.
Setting a fixed price on a product
You set a fixed rate where you create or change the product — in Apostra chat, in the ad-server source widget, or through the API — by giving it a pricing option:delivery_type on the option itself. The option’s delivery type
is the one that counts, and an omitted one becomes non_guaranteed — so a
guaranteed product whose pricing option leaves it out is sold as non-guaranteed,
whatever the product says. Set it on every option you write.
Leaving pricing off is not the same as setting it later. A product created
with no pricing option gets one auction CPM option with no floor and no price
guidance. Buyers see that option and can bid on it, but you have not told them
what the product is worth. If you know your rate, set it when you author the
product.
A fixed rate is the right choice when one ad-server selector needs more than
one price — a single ad unit serving both display and video, for example. The
uploaded feed keys each row on the selector, so it gives every product on that
selector the same price; a rate set on the product is per-product, so a display
product and a video product over the same ad unit can be priced differently.
Create one product per format and set each one’s rate.
The rate must be in your settlement currency. If it isn’t, the product is
created but not offered on that price (it stays hidden unless it also carries an
auction option), and the response carries a warning naming the
mismatch — see A wholesale feed is single-currency.
After a product create, update, or delete succeeds, Apostra invalidates
the source’s derived wholesale catalog and queues a fresh read from the sales
agent. Buyers and readiness therefore pick up the accepted product change
without a separate manual refresh; if that background read is temporarily
unavailable, normal catalog recovery retries it without treating the prior
projection as current. A burst of accepted authoring writes is coalesced into
one fresh read after the last accepted change.
A missing, invalid, or stale feed warns you; it never takes you off the
market. The wholesale pricing readiness check reports it, and your products
that carry an auction option stay listed to buyers, now without the guidance
the feed would have supplied. The check never blocks going live and never
withholds a source from buyers. A feed whose rows are current and in-window but
whose selectors don’t match any product that needs them warns the same way — it
doesn’t count as current, because it isn’t pricing anything (see
Selectors that don’t match any product).
An operator fixed price in your settlement currency never trips this
check — it’s a complete price on its own (see above), so a source you’ve
priced entirely with fixed rates is never warned about an upload you don’t need.
Your rows’ own effective_end_date is what sets the expiry. A row prices
products while it is in window and stops when the window you declared closes,
so a feed covering a quarter stays current for that quarter without a
re-upload. Once a source has no in-window rows left, its manually priced
products lose that guidance until you upload a refreshed file. They stay
listed as auction products with no floor and no price guidance.
Starting 7 days before that expiry, readiness switches from silent to an
advance warning — “pricing guidance starts expiring in N days — re-upload to
keep it current” — so you have a week’s notice before the guidance lapses,
rather than finding out only after it does. The countdown is to your
soonest-expiring row, so on a feed whose rows end on different dates it tells
you when the first of them goes, not the last. Neither this warning nor the
expiry itself blocks going live. You also get one notification naming the date,
with an action to upload a current feed (see
Wholesale pricing expiring soon).
There is one backstop behind your dates: an accepted upload is honoured for at
most 365 days, however far out its rows reach. It exists so a feed uploaded
once and then abandoned can’t price inventory indefinitely — it is not a
routine refresh clock, and a feed re-uploaded each period never meets it. A
declared window longer than 180 days is accepted as written but returns an
advisory on upload, because CPM guidance that old rarely still reflects the
market. The pre-filled template uses a
quarter-long window for that reason.
Pricing-related source health shows how many active products are ad-server
priced, manually priced, or still unresolved. It asks for an upload only for
the unresolved products. This applies only to wholesale pricing: your ad-server
connection still needs to be connected for inventory sync, trafficking,
delivery, and reporting.
Readiness measures what buyers can actually purchase, not just what you have
created. Buyers asking with a brief get products composed for them, and
composition needs a price to build on: a fixed price, or a floor from your
uploaded feed or ad-server guidance. A product listed only as an auction with no
floor and no price is still offered to buyers browsing your wholesale catalog,
but it is not composed into an answer to a brief, so it does not count here.
When none of your products has a price or floor buyers can buy against, the
Products available readiness check blocks going live and reports the
condition directly (for example, “3 products created, 0 visible to buyers —
pricing unresolved”) with a link to upload pricing and availability. Setting a
fixed price or a floor on any one product clears it. A Seller Account does not
report itself ready to transact while buyers can buy none of the products you
created.
When the reason is specifically a currency mismatch — every hidden product’s
only problem is a fixed price or feed in the wrong currency — readiness names
that instead (for example, “3 products created, 0 visible to buyers — priced
in the wrong currency”), so you fix the actual blocker rather than treating it
as a generic pricing gap.
Suppressed markets are visible, not silent
A country dropped by the minimum-line-item suppression (see How pricing is used) is not just absent from the product — it’s named. The merged product’sext.wholesale_suppressed_countries
lists every country suppressed for that product, and the wholesale-pricing
readiness check’s details enumerate suppressed markets across a source (for
example, “suppressed markets: ID, IN”) so you can tell “this market has no
avails in the feed” apart from “this market was priced but judged unbuyable
against your minimum.”
An unpriceable product stays in your catalog, not just in buyer results
A product your pricing feed can’t price at all — not a suppressed market within an otherwise-priced product, but the whole product — stays in your catalog view instead of disappearing. It’s markedext.wholesale_priced: false
with a reason on ext.wholesale_unpriced_reason:
feed_stale— your uploaded feed is past the 365-day backstop (see Readiness and freshness).no_selectors— the product doesn’t declare the ad-server placement/unit identifiers the feed prices against.no_matching_pricing_rows— your feed has no row for this product’s placement.mixed_currency— the feed rows that matched this product span more than one currency; prices in different currencies can’t be blended into one floor.all_countries_suppressed— every country this product could sell in was suppressed by the minimum-line-item check (see Suppressed markets are visible, not silent above).incompatible_pricing_option— the product’s existing pricing option isn’t one the feed can safely overwrite (for example, a legacy fixed-price or non-CPM option).
$0 is never shown as a
price. It stays enumerable to you so you can see why it carries no guidance,
instead of it silently vanishing from your product count. A product with no
pricing option at all stays hidden. The marker never
overrides a price you set yourself: a product with an operator fixed price in
your settlement currency remains buyable on that price even when the feed
couldn’t price it (the same fixed-price rule as
Readiness and freshness — a fixed rate is a
complete price on its own).