Skip to main content
An ad server may expose eligible wholesale pricing for some products but not others — or, depending on the ad server, for none at all. Interchange derives non-guaranteed pricing automatically where the ad server’s reporting supports it (today that is Google Ad Manager only — see Which ad servers need this); products the ad server cannot price still need an explicit price before they can be sold. The wholesale avails & pricing module closes that gap. You upload a small, structured feed — one row per ad-server selector (a GAM ad unit, a SpringServe supply tag, a FreeWheel site section, an AdsWizz zone, …) — giving your percentile CPMs and monthly availability. The platform validates it deterministically, stores it with provenance and a freshness date, and uses it only for products whose ad-server pricing is unresolved. Existing ad-server prices are preserved. Your ad server still handles execution.
This is a storefront-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.
You never need an internal Scope3 product id. Your ad-server selector — the id your ad server already uses for the inventory — is the key. It’s the same id (and selector type) the selector browser shows you, so it maps cleanly to the products it backs.

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.

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:
When selector_type is omitted, the upload assumes ad_unit — which is only correct for GAM ad units. On any other ad server, always fill in selector_type explicitly. A row with the wrong (or defaulted) selector type is accepted by validation but matches none of your products, so the pricing silently never applies. Before uploading, check which selector types your products actually use — they are shown on each product and in the selector browser.

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.
An inventory export or targeting map is not enough by itself. Those files are useful for the ad_server_selector column, but the upload also needs country, currency, CPM percentiles, effective dates, and a source version.

Required columns

Optional columns

Any other columns are preserved as metadata and ignored by validation.

GLOBAL or per-country, not both

A GLOBAL 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 a selectorDiffAvailable flag telling you whether the feed’s selectors could be checked against the source’s current products:
  • selectorDiffAvailable: true — the response also reports matchedSelectorCount, unmatchedSelectorCount, and unmatchedSelectors (the first 5 unmatched selectors, named; the count carries the full total). A row can pass every validation rule and still price nothing if its ad_server_selector or selector_type doesn’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 against browse_ad_server_selectors or 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 and selectorDiffNote explains 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

Interchange 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 storefront’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 storefront hasn’t confirmed a settlement currency yet, every row must still share one currency; a file with more than one is rejected. Once your storefront 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 storefront and never makes the product buyable, even though the price itself is otherwise valid.
If a currency mismatch is why buyers can’t see a product, the Products available readiness check says so directly rather than reporting a generic “pricing unresolved” — see Readiness and freshness.

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. Where p25 is 0 (run-of-network display that clears at $0 part of the time), the floor is 0 and the product sells at open auction, guided by p50/p75.
  • p50 and p75 are 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.
Inventory that can’t be bought isn’t offered. A country is shown only if a buyer could actually purchase it: if its available impressions are too thin to reach your minimum line item without an unrealistic CPM, that country is left off the product. Your minimum line item — the smallest line-item spend worth booking — is a storefront setting; the platform reads it from your ad server when it can, otherwise you set it.

Example feed

Copy this template into a CSV file. The columns are identical for every ad server — only the ad_server_selector ids and selector_type values differ. For a Google Ad Manager source:
For a SpringServe source (note the explicit selector_type):
For a FreeWheel source:
For an AdsWizz source:

Uploading

Upload from Murph chat

Attach the completed CSV or XLSX feed in Murph and identify it as a wholesale pricing upload for the inventory source. Murph 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 Murph to commit it and use the Approve control. Nothing is written before approval. Murph 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, Murph 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 modular availability feeds use their respective setup flows.

Upload through the API

All examples use the storefront base URL:

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 90-day 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) without confirmed to see exactly what will be accepted or rejected before anything is written:
The response reports 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 storefront’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 with confirmed: 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 storefront 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.
An .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, Interchange 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. A product is shown to buyers when any of its three pricing paths succeeds: eligible ad-server pricing, a current uploaded feed, or a fixed price set directly on the product — a pricing option with is_fixed: true, a positive rate, and a currency matching your storefront’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 the product stays hidden until the price is set in your settlement currency (the authoring surface warns you when this happens). Auction-priced products (no fixed rate) still need one of the first two paths, because open-auction selling requires the percentile guidance and availability those paths provide. After a product create, update, or delete succeeds, Interchange 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 keeps only the products that depend on that fallback off the market. A feed whose rows are current and in-window but whose selectors don’t match any product that needs them behaves 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 blocked on an upload you don’t need. The most recent accepted feed goes stale after ~35 days; once stale, your manually priced products come off the market until you upload a refreshed file. Keeping the feed current is what keeps those products sellable. Starting 7 days before that cliff, readiness switches from silent to an advance warning — “pricing feed expires in N days — re-upload to keep products visible” — so you have a week’s notice before anything actually vanishes, rather than finding out only after it does. This warning never blocks going live; it’s advisory until the feed actually goes stale. Note the pre-filled template sets a 90-day effective_start_date/effective_end_date window on each row — that’s a planning horizon for the pricing itself, not the platform’s staleness clock. Even a row whose effective window still has months left goes stale (and drops out) after ~35 days if you haven’t re-uploaded, because the feed-freshness check and the row’s own effective dates are two independent things. 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. When every product on your storefront is hidden from buyers because its pricing is unresolved, 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. A storefront no longer reports itself ready to transact while every product you created is hidden from buyers. 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’s ext.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 marked ext.wholesale_priced: false with a reason on ext.wholesale_unpriced_reason:
  • feed_stale — your uploaded feed is more than ~35 days old (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).
A product marked this way with no other real price is never offered to buyers — the marker means the feed left it with only a placeholder. It stays enumerable to you so you can tell it exists and see why it isn’t selling, instead of it silently vanishing from your product count. 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).