> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apostra.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

An ad server may expose eligible wholesale pricing for some products but not
others — or, depending on the ad server, for none at all. Apostra 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](#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.

<Note>
  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.
</Note>

You never need an internal Apostra 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:

| Ad server         | Automatic ad-server pricing           | What the uploaded feed does                                      |
| ----------------- | ------------------------------------- | ---------------------------------------------------------------- |
| Google Ad Manager | Yes — derived from delivery reporting | Fallback for products the ad server could not price              |
| SpringServe       | Not yet                               | The only wholesale pricing source — every product needs the feed |
| FreeWheel         | Not yet                               | The only wholesale pricing source — every product needs the feed |
| AdsWizz           | Not yet                               | The only wholesale pricing source — every product needs the feed |

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:

| Ad server         | Valid `selector_type` values                                                                                      |
| ----------------- | ----------------------------------------------------------------------------------------------------------------- |
| Google Ad Manager | `ad_unit`, `placement`                                                                                            |
| SpringServe       | `supply_partner`, `supply_router`, `supply_tag`, `key`, `value_list`                                              |
| FreeWheel         | `site`, `site_section`, `site_group`, `series`, `video_group`, `ad_unit`, `ad_unit_package`, `standard_attribute` |
| AdsWizz           | `zone_group`, `zone`, `supply_pack`                                                                               |

<Warning>
  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
  never applies. The Inventory Feed Task flags unmatched selectors in preview;
  before committing, check their ids and types against the selector browser.
</Warning>

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

| Filter            | Values                    |
| ----------------- | ------------------------- |
| `mediaType`       | `display`, `video`        |
| `environmentType` | `BROWSER`, `VIDEO_PLAYER` |

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.

<Card title="Download your pre-filled template" icon="table" href="#0-download-a-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.
</Card>

<Warning>
  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.
</Warning>

### Required columns

| Column                                       | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ad_server_selector`                         | The ad server's stable selector id, exactly as the selector browser shows it. For GAM: the numeric **Ad Unit ID** (GAM → Inventory → Ad units) or a placement id. For SpringServe: a supply tag, supply router, supply partner, key, or value-list id. For FreeWheel: a site, site-section, or other node id. For AdsWizz: a zone, zone-group, or supply-pack id. See [Selector ids and types by ad server](#selector-ids-and-types-by-ad-server).                          |
| `country`                                    | A country name (`United States`, `Germany`, …), an ISO-3166 alpha-2 code (`US`, `GB`, `AU`), or `GLOBAL`. One row per selector per country — see [GLOBAL or per-country, not both](#global-or-per-country-not-both).                                                                                                                                                                                                                                                        |
| `currency`                                   | A real ISO-4217 currency code (e.g. `USD`, `EUR`, `GBP`). Reserved or placeholder codes such as `ZZZ` are rejected. **Every row in a feed must use the same currency, and it must match your Seller Account's confirmed settlement currency** — see [A wholesale feed is single-currency](#a-wholesale-feed-is-single-currency).                                                                                                                                            |
| `pricing_model`                              | `cpm`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `p25_cpm`, `p50_cpm`, `p75_cpm`              | Your clearing-price distribution: the 25th, 50th (median), and 75th percentile CPMs. Non-decreasing (`p25 ≤ p50 ≤ p75`). `p25`/`p50` may be `0` (common for run-of-network display that clears at \$0 part of the time); `p75` must be greater than 0 so the row has a sellable price. Scientific notation (e.g. `7.7e-7`) is accepted. Values above `1,000` are rejected as likely unit mistakes or fat fingers; contact Apostra if a CPM above that limit is intentional. |
| `effective_start_date`, `effective_end_date` | The window this pricing is valid for, ISO `YYYY-MM-DD` (end after start).                                                                                                                                                                                                                                                                                                                                                                                                   |
| `source_version`                             | Any label for the file version, e.g. `2026-Q3`.                                                                                                                                                                                                                                                                                                                                                                                                                             |

### Optional columns

| Column                | Meaning                                                                                                                                                                                                                                                                                                |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `selector_type`       | One of your ad server's selector types — see [Selector ids and types by ad server](#selector-ids-and-types-by-ad-server). If omitted, the upload assumes `ad_unit` (GAM ad units only) — **on SpringServe, FreeWheel, and AdsWizz, always set it explicitly**, or the rows will not match any product. |
| `selector_name`       | Human name of the selector, for verification (e.g. `Run of Site — Display`).                                                                                                                                                                                                                           |
| `monthly_impressions` | **Monthly** available impressions for this selector and country, whole number. (`available_impressions` / `avails` are also accepted.) Values above `100,000,000,000` per selector/country/month are rejected as likely unit mistakes; split the row or contact Apostra if that volume is intentional. |
| `format_option_ref`   | JSON identifying the canonical format this row prices. Use this only when one selector backs products with different formats; see [Format-specific pricing behind a shared selector](#format-specific-pricing-behind-a-shared-selector).                                                               |

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-scoped `format_option_ref` to keep their prices separate. The value
is JSON stored in the CSV or XLSX cell:

```json theme={null}
{
  "scope": "publisher",
  "publisher_domain": "united-online.example",
  "format_option_id": "<display-format-option-id>"
}
```

Both `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:

| Product format | `ad_server_selector` | `format_option_ref`                                                                                                | `p75_cpm` |
| -------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------ | --------- |
| Display        | `shared-selector`    | `{"scope":"publisher","publisher_domain":"united-online.example","format_option_id":"<display-format-option-id>"}` | `9.00`    |
| Video          | `shared-selector`    | `{"scope":"publisher","publisher_domain":"united-online.example","format_option_id":"<video-format-option-id>"}`   | `16.00`   |

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:

| Diagnostic                                  | What to fix                                                                                                                      |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `WHOLESALE_V1_UNRESOLVED_FORMAT_OPTION_REF` | Correct the publisher domain or format option id so it matches a product on this source.                                         |
| `WHOLESALE_V1_AMBIGUOUS_FORMAT_SCOPE`       | Remove a scoped/unscoped mix, or separate scoped rows whose effective windows overlap for the same resolved product and country. |
| `WHOLESALE_V1_UNSUPPORTED_DEVICE_SCOPE`     | Remove the device column and use catalog product/format identity instead.                                                        |

<Note>
  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.
</Note>

## 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](#selector-ids-and-types-by-ad-server)), 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](#readiness-and-freshness). If you see
  unmatched selectors after a commit, re-check the ids against
  [`browse_ad_server_selectors`](#selector-ids-and-types-by-ad-server) 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

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](#0-download-a-pre-filled-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](#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.

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](#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 Seller Account 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:

```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
111942652,ad_unit,Run of Site Display,US,USD,cpm,4.00,6.00,9.00,12000000,2026-07-01,2026-09-30,2026-Q3
111942652,ad_unit,Run of Site Display,GB,USD,cpm,3.00,5.00,8.00,3000000,2026-07-01,2026-09-30,2026-Q3
289471120,ad_unit,6s 15s Video Pre-roll,GLOBAL,USD,cpm,10.00,14.00,20.00,4500000,2026-07-01,2026-09-30,2026-Q3
```

For a SpringServe source (note the explicit `selector_type`):

```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,NL,EUR,cpm,12.00,16.00,22.00,8000000,2026-07-01,2026-09-30,2026-Q3
482913,supply_tag,CTV Pre-roll Direct,BE,EUR,cpm,10.00,14.00,19.00,2500000,2026-07-01,2026-09-30,2026-Q3
75210,supply_router,Web Video Router,GLOBAL,EUR,cpm,6.00,9.00,13.00,15000000,2026-07-01,2026-09-30,2026-Q3
```

For a FreeWheel source:

```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
1088231,site_section,Sports Live CTV,US,USD,cpm,18.00,24.00,32.00,6000000,2026-07-01,2026-09-30,2026-Q3
447015,series,Morning News Show,US,USD,cpm,14.00,19.00,26.00,2000000,2026-07-01,2026-09-30,2026-Q3
```

For an AdsWizz source:

```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
55401,zone,Drive Time Podcasts,DE,EUR,cpm,8.00,11.00,15.00,9000000,2026-07-01,2026-09-30,2026-Q3
2310,zone_group,All Music Streams,GLOBAL,EUR,cpm,5.00,7.50,11.00,20000000,2026-07-01,2026-09-30,2026-Q3
```

## 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 cards, Playbook
sell-side pricing rules, and buyer discounts remain in their owning surfaces.

### 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 buyer
discounts use their respective setup flows.

### Upload through the API

All examples use the legacy v2 `storefront` base URL for Seller Accounts:

```bash theme={null}
export BASE=https://api.apostra.com/api/v2/storefront
export SCOPE3_API_KEY=scope3_<your_api_key>
```

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

```bash theme={null}
curl "$BASE/inventory-sources/$SOURCE_ID/wholesale-pricing/template" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" -o wholesale-pricing-template.csv
```

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

```bash theme={null}
curl -X POST "$BASE/inventory-sources/$SOURCE_ID/wholesale-pricing/module" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

### 2. Preview

Submit rows (or paste CSV) **without** `confirmed` to see exactly what will be
accepted or rejected before anything is written:

```bash theme={null}
curl -X POST "$BASE/inventory-sources/$SOURCE_ID/wholesale-pricing" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "csvText": "ad_server_selector,selector_type,country,currency,pricing_model,p25_cpm,p50_cpm,p75_cpm,effective_start_date,effective_end_date,source_version\n111942652,ad_unit,US,USD,cpm,4.00,6.00,9.00,2026-07-01,2026-09-30,2026-Q3" }'
```

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
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](#selectors-that-dont-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 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.

```bash theme={null}
curl -X POST "$BASE/inventory-sources/$SOURCE_ID/wholesale-pricing" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "confirmed": true, "csvText": "..." }'
```

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

### Setting a fixed price on a product

You set a fixed rate where you create or change the product — in Murph chat, in
the ad-server source widget, or through the API — by giving it a pricing
option:

```json theme={null}
"pricing_options": [
  {
    "pricing_model": "cpm",
    "is_fixed": true,
    "rate": 3.00,
    "currency": "BRL",
    "delivery_type": "non_guaranteed"
  }
]
```

**Always set `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, which is not
a complete price — the product is created, but buyers never see it until one of
the three pricing paths succeeds. 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 stays hidden, and the response carries a warning naming the
mismatch — see [A wholesale feed is single-currency](#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 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](#selectors-that-dont-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.

**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 come off the market until you upload a refreshed file.

Starting **7 days before that expiry**, readiness switches from silent to an
advance warning — "pricing starts expiring 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. 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. This warning never blocks
going live; it's advisory until the pricing actually expires.

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](#0-download-a-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. When every product on your Seller Account 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 Seller Account 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](#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 past the 365-day backstop (see
  [Readiness and freshness](#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](#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](#readiness-and-freshness) — a fixed rate is a
complete price on its own).
