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

# Wholesale avails & pricing

> Fill wholesale pricing and availability gaps by uploading a percentile-CPM feed keyed on your ad-server selectors

Some products on an ad-server source have no price the platform can sell them at. 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, …) with your percentile CPMs and monthly availability. The platform validates it, stores it
with a freshness date, and uses it **only for products whose own pricing is unresolved**. A price
already set on a product is kept.

<Note>
  The module is opt-in per ad-server source: registering the feed or your first committed upload
  attaches it. Modular sources use the [static avails feed](/sell/modular-sources) instead.
</Note>

<Warning>
  No ad-server adapter runs on Semicola yet, so no product is synced with ad-server selectors. Until
  one does, every preview and commit reports the selector match as unavailable
  (`selectorDiffAvailable: false`). That is not a mismatch: the check runs once products exist.
</Warning>

## What you supply

One row per **ad-server selector × country**. Column order and header capitalization don't matter;
CSV and XLSX both work.

| Column                                       | Meaning                                                                                                                                                                                        |
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ad_server_selector`                         | The ad server's selector id, exactly as your products reference it.                                                                                                                            |
| `selector_type`                              | Optional. Defaults to `ad_unit`, which is only right for GAM ad units. On SpringServe, FreeWheel and AdsWizz always set it: a wrong type is accepted but matches no product.                   |
| `selector_name`                              | Optional human name, for verification.                                                                                                                                                         |
| `country`                                    | A country name, an ISO-3166 alpha-2 code, or `GLOBAL`.                                                                                                                                         |
| `currency`                                   | A real ISO-4217 code, the same on every row and equal to your confirmed settlement currency. There is no FX.                                                                                   |
| `pricing_model`                              | `cpm`.                                                                                                                                                                                         |
| `p25_cpm`, `p50_cpm`, `p75_cpm`              | Non-decreasing. `p25`/`p50` may be `0`; `p75` must be above 0. Values above 1,000 are rejected.                                                                                                |
| `monthly_impressions`                        | Optional whole number (`available_impressions` and `avails` are accepted). Above 100,000,000,000 is rejected.                                                                                  |
| `effective_start_date`, `effective_end_date` | ISO `YYYY-MM-DD`, end after start. A window over 180 days is accepted with an advisory.                                                                                                        |
| `source_version`                             | Any label, for example `2026-Q3`.                                                                                                                                                              |
| `format_option_ref`                          | Optional JSON `{"scope":"publisher","publisher_domain":…,"format_option_id":…}` to price display and video products behind one selector separately. Use it on every row of a selector or none. |

A selector uses **either one `GLOBAL` row or only country rows**, never both: a mix rejects every row
of that selector with `GLOBAL_COUNTRY_CONFLICT`. Device columns (`device_type`, `Device Type`) are
rejected with `WHOLESALE_V1_UNSUPPORTED_DEVICE_SCOPE`.

```csv theme={null}
ad_server_selector,selector_type,selector_name,country,currency,pricing_model,p25_cpm,p50_cpm,p75_cpm,monthly_impressions,effective_start_date,effective_end_date,source_version
482913,supply_tag,CTV Pre-roll Direct,US,USD,cpm,12.00,16.00,22.00,8000000,2026-07-01,2026-09-30,2026-Q3
75210,supply_router,Web Video Router,GLOBAL,USD,cpm,6.00,9.00,13.00,15000000,2026-07-01,2026-09-30,2026-Q3
```

## Upload from the ad-server source

Open **Get ready to sell**, find the ad-server source, and use **Import seller feed** in its
**Wholesale price & avails** section. This opens the shared **Import inventory feed** task for that
source:

1. **Download pre-filled template**, then add the CPM percentiles and monthly impressions.
2. Upload the completed CSV or XLSX, or paste the rows, and **Preview**. Nothing is written. You see
   the accepted and rejected row counts, each rejected row with its row number, column and why, any
   advisories, and the selector match (or why it's unavailable).
3. **Commit** stays disabled while any row is rejected. The commit is tied to the file you previewed
   on the source you chose: editing the rows or switching the source clears the preview, and a
   changed file is refused until you preview it again. The commit fully replaces the source's prior
   feed and repeats the selector match, so a feed that prices nothing is still flagged.

The same task imports a modular source's static avails feed when it's opened from that source. It
isn't a general pricing editor: rate cards and buyer discounts keep their own flows.

The section itself shows where the source stands:

* the active products that are ad-server or fixed priced, manually priced (your upload), and
  unresolved, with a request to upload pricing for the unresolved ones;
* "N products created, 0 visible to buyers — pricing unresolved" when every product is hidden for
  its price;
* the committed feed (rows, selectors, source version, when) or that none is committed yet;
* the advance warning from 7 days before the soonest row expires, and any suppressed markets;
* your **Minimum line item**, which you can edit there (it attaches the module).

## Upload from chat

Attach the completed CSV or XLSX and say it's a wholesale pricing upload (name the source if your
storefront has more than one ad-server source; Semi lists them otherwise). Semi runs the same
preview, writes nothing, and reports the accepted and rejected counts, every rejected row (row,
column and why) and which selectors matched a product, or that the match is unavailable because no
products have synced. Because the turn reads your file, Semi asks you to **Approve** before it runs
the preview.

Once the preview has zero rejected rows, ask Semi to commit it and **Approve**. The commit reads the
file from where it was stored when you attached it, so approving on a later message commits exactly
that file. A commit is refused while any row is rejected, and fully replaces the source's prior
feed. Use this only for the wholesale pricing template.

## Upload through the API

All paths are under `/api/v2/storefront`.

1. **Template (optional):** `GET /inventory-sources/{sourceId}/wholesale-pricing/template` returns a
   CSV with one `GLOBAL` row per selector your products reference, your settlement currency, `cpm`,
   the current quarter and a dated `source_version`. Fill in the percentiles and impressions.
2. **Module (optional):** `POST /inventory-sources/{sourceId}/wholesale-pricing/module` with
   `{"minimumLineItemSpend": 5000}` sets your minimum line item up front.
3. **Preview:** `POST /inventory-sources/{sourceId}/wholesale-pricing` with `csvText` (or `rawRows`)
   and no `confirmed`. The response has `acceptedRowCount`, `rejectedRowCount`, a `rejected` list
   (row, column, code, why), `advisories`, and the selector-match fields.
4. **Commit:** the same request with `"confirmed": true`. A commit is refused while any row is
   rejected. It **fully replaces** the source's prior feed: a selector you leave out loses its
   uploaded fallback. The response repeats the selector-match fields, so check
   `unmatchedSelectorCount` rather than treating a 200 as proof the feed priced something.

The canonical feed transport works too: register `wholesale-avails-pricing` `v1` with
`POST /inventory-sources/{sourceId}/feeds`, then `feed/preview` (or `feed/upload` for a file) and
`feed/commit` with the signed baseline, exactly as for the [static feed](/sell/modular-sources).
Each wholesale preview and commit carries a `wholesale` summary with the same fields.

### Selector match

* `selectorDiffAvailable: true`: `matchedSelectorCount`, `unmatchedSelectorCount` and
  `unmatchedSelectors` (the first 5, named). A committed feed whose selectors match nothing still
  succeeds, but it prices nothing.
* `selectorDiffAvailable: false`: the source has no products with selectors yet; `selectorDiffNote`
  says so.

## How pricing is used

* The **floor comes from `p25`**; `p50` and `p75` are guidance. A `p25` of `0` sells at open auction.
* Prices and availability stay **per country**: the product carries a forecast per priced country.
* A country whose month of avails at `p75` can't reach your **minimum line item** is suppressed and
  named in the product's `ext.wholesale_suppressed_countries`.
* A product the feed can't price stays in your catalog with `ext.wholesale_priced: false` and a
  reason: `feed_stale`, `no_selectors`, `no_matching_pricing_rows`, `mixed_currency`,
  `all_countries_suppressed` or `incompatible_pricing_option`. Buyers don't see it.
* A product with its own complete price in your settlement currency is never repriced by the feed.

## Readiness and freshness

Your rows' `effective_end_date` sets the expiry; an accepted upload is honoured for at most 365 days.
The **Wholesale pricing** check on the setup rail counts active ad-server products with their own
price, priced by your upload, and unresolved. From 7 days before the soonest row expires it warns
"pricing starts expiring in N days — re-upload to keep products visible", and it lists suppressed
markets. When every product is hidden for pricing, **Products ready to sell** says
"N products created, 0 visible to buyers — pricing unresolved".

## Not available yet

* API push (`v1-push`) for the wholesale feed, and the selector browser
  (`browse_ad_server_selectors`).
* Automatic ad-server pricing and selector sync: they need an ad-server adapter.

## Related

* [Inventory sources](/sell/inventory-sources)
* [Modular inventory sources](/sell/modular-sources)
* [Products](/sell/products)
