---
title: "Configure the self-serve campaign catalogue"
description: "The admin Self-serve group — the prototype banner each self-serve campaign type is cloned from, and the country × OS CPM floor matrix that feeds the suggested price the REST API hands an external self-serve front end."
type: "howto"
audience: [admin]
keywords: [self-serve, prototype banner, proto banner, CPM floor, floor rate, suggested CPM, minimal CPM, self-serve campaign type, CPM floor adjustment, white label]
ui_path: "Admin → Self-serve"
---

Fetch the complete documentation index at: https://epom.com/ad-server/docs/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# Configure the self-serve campaign catalogue

The **Self-serve** admin group holds two catalogues that a self-serve advertiser front end
reads over the REST API: the **prototype banner** that new creatives of each self-serve
campaign type are cloned from, and the **CPM floor matrix** that the API turns into a
suggested price.

Both are configuration for an *external* client. Epom ships no self-serve advertiser UI —
nothing in the product lets an advertiser pick a self-serve campaign type and get a
creative. The self-serve storefront is your own
application, and it calls `/rest-api/prototype/*`, `/rest-api/floorCPMs*` and
`/rest-api/banner/create`. These two admin pages are what those endpoints answer with.

> **Note:** neither page changes ad serving. The floor rates are advice returned to an API
> caller, not a bid filter — see [The floor is advice, not a
> gate](#the-floor-is-advice-not-a-gate).

Both pages are keyed on the same seven **self-serve campaign types**: Banner (desktop),
Banner (mobile), Banner (app), Native, Pop-under, Interstitial (mobile), Interstitial (app).

The same type appears in two other places. A campaign record carries one, and it is a REST
parameter on the banner, campaign and floor-rate endpoints, which accept either the enum name
(`BANNER_DESKTOP`) or the numeric ID (`1`) for backward compatibility. The current console
draws no control for the campaign's type: the **Basic info** card keeps the stored value and
sends it back unchanged on save, so the way to set it is the REST `selfServeCampaignType`
parameter on `POST /rest-api/campaign/create.do` and `POST /rest-api/campaign/{campaignId}/update.do`.
The type stored on a campaign is a label; it does not by itself clone anything.

## Who this is for

Network administrators. Each page is a separate admin permission — see
[Manage users, roles and permissions](https://epom.com/ad-server/docs/guides/admin/users-and-roles.md). A role may hold one and not the
other; the **Self-serve** group disappears from the admin sidebar when it holds neither. See
also [Per-role notes](#per-role-notes).

## Prerequisites

- The seed **banner** already exists as a normal creative under a live campaign and
  advertiser, and you know its numeric ID. The form takes an ID, not a picker.
- The optional **default conversion action** also already exists, and is not a
  shared-by-default action.
- For floor rates, a CSV file — it is the only way to add a country to a matrix.

## Steps

The two settings are independent; configure the one you need.

### Self-serve campaign prototype banners

A prototype pairs one self-serve campaign type with one seed banner. There is a unique key
on the campaign type, so the table holds **at most seven rows — one per type**, and a
second prototype for a type you already covered is refused with *Duplicate entry.*

#### What actually gets cloned

When your storefront calls `POST /rest-api/banner/create` with a `selfServeCampaignType`
parameter, the server looks up that type's prototype and clones **the banner**. Nothing else
is cloned: the campaign must already exist and is named by the request's `campaignId`, and the
new banner belongs to that campaign and therefore to that campaign's advertiser.

| | |
| --- | --- |
| **Carried over from the prototype** | Creative type and payload (code, link, image / video / file references, title), size and ad unit, placement type, targeting rules, frequency / click / action / no-click capping, limits, pricing, price-adjustment rules, template and template property values, VAST and native configuration, tracking code, labels and autolink settings, approval status, active flag |
| **Not carried over** | Placement links — the clone starts unlinked; the prototype's name (replaced by the `name` in the request); the prototype's campaign and advertiser; **the default conversion action** |

> **Warning:** the prototype's **Default conversion action** is never applied to the cloned
> banner. The server stores it, publishes it over `/rest-api/prototype/list`,
> `/rest-api/prototype/banner/{type}` and `/rest-api/prototype/banner/action`, and stops
> there. It is a value for your storefront to read and act on — if your client ignores it,
> nothing wires it up.

Two failures your client will see:

- No prototype configured for the requested type → **400** *No available prototypes for
  campaign type [BANNER_NATIVE].*
- The prototype's banner has been deleted → **400** *Banner id not found.*

The read endpoints answer differently: `GET /rest-api/prototype/{prototypeId}` and
`GET /rest-api/prototype/banner/{type}` return **404** naming what is missing (*Prototype N not
found.* / *No prototype for campaign type …*).

#### Manage prototypes

1. Go to **Admin → Self-serve → Self-serve campaign prototype banners**. The grid heading
   reads **Self-Serve prototype banners** and the columns are **CampaignType**, **Banner
   id** and **Default conversion action**. Both entity columns render as `[id: N] name`;
   an unset default action shows `—`.
   ![Self-Serve prototype banners grid, one row per self-serve campaign type](https://epom.com/docs-images/ad-server/self-serve/proto-banners-grid.webp)
2. Click **Add** (the page becomes *Add prototype banner*), or tick a row and click **Edit**
   (*Edit prototype banner*).
3. Fill the three fields — **Self-Serve type**, **Prototype banner ID**, **Default
   conversion action ID** — and click **Save**. A successful save shows a **Done** toast and
   returns you to the grid. **Cancel** goes back without saving.
4. To remove a prototype, tick its row and click **Delete**, then confirm *Delete record?*.

Deleting a prototype does not touch the banner it pointed at.

### Self-serve CPM floor rates

One matrix per campaign type. Rows are **countries**, columns are five fixed **operating
systems** — Windows, Linux, Mac OS X, Android, iOS — and each cell is a CPM floor rate for
that country/OS/type combination.

#### Add or change rates

1. Go to **Admin → Self-serve → Self-serve CPM floor rates**. The page lists the seven
   campaign types; click **Edit** on one.
2. The matrix opens at **Edit CPM floor rates — Banner (desktop)** (or whichever type you
   picked). A country whose code is `*` is shown as **Global**; a cell with no rate shows `—`.
   Rows come back in no particular order, so **Global** can sit anywhere in the list.
   ![Self-serve CPM floor-rate matrix, countries by operating system](https://epom.com/docs-images/ad-server/self-serve/floor-matrix.webp)
3. **To add countries, click Upload** and pick a CSV. This is the only way to create a
   row — there is no "add country" control anywhere on the page.
4. **To change a rate that already exists**, click the cell, type the new value and press
   Enter or click away. The cell saves on its own; there is no page-level Save, and no
   confirmation — a successful edit just leaves the new number in the cell.
5. To remove countries, tick their rows and click **Delete**. Confirm *Delete N record(s)?*
   and you get an *Entries has been removed.* toast. A delete removes that country's rates
   for **all five operating systems**, in the campaign type you are editing only.

#### CSV format

Three comma-separated columns in the order *country, operating system, rate*. UTF-8, and
**no header row** — a header line is read as data and rejected as an invalid record.

- **country** — an ISO 3166-1 alpha-2 code, or `*` for global.
- **os** — the operating system's **display name**, spelled exactly: `Windows`, `Linux`,
  `Mac OS X`, `Android`, `iOS`. The parser matches a *fragment* of the name,
  case-sensitively, so `Android 14` resolves to Android; but `iPad` resolves to iPadOS and
  `Windows Phone` to Windows Phone, which are not among the five.
- **rate** — a number, zero or greater.

```csv
US,Windows,0.90
US,Linux,0.85
US,Mac OS X,1.10
US,Android,1.25
US,iOS,1.40
*,Windows,0.40
*,Linux,0.35
*,Mac OS X,0.50
*,Android,0.55
*,iOS,0.60
```

Every country in the file must carry a rate for **all five** operating systems (an extra
operating system such as Windows Phone is saved for a complete country but never shown in the
five-column matrix — `suggest` without an `os` still counts it). A country
short of one is dropped in full, and the upload reports *Records for the following countries
were ignored because they don't have rates for all required operating systems: …*.

Other rejections you will see, one message per offending line: *Invalid line: [ZZ, WINDOWS,
1.10]* (the operating system did not resolve, or the country is empty, or the rate is not a
number), *Invalid country code: XX*, *Invalid rate value: -1.0* for a negative rate, and
*Invalid record: […]. Expected: country, os, rate* when a line does not have exactly three
columns.

> **Warning:** a rejected upload still ends with a green **Done**. The error toast and the
> success toast appear together, because "done" only means the file was received. Read the
> error toast, then check the grid — never take **Done** as proof that anything was written.

#### How a rate becomes a price

Your storefront asks `GET /rest-api/floorCPMs/suggest` for a campaign type and advertiser,
optionally narrowed by `os` and `geo`. The server collects every matching row, takes the
**single highest rate** among them, and returns two numbers:

| Figure | How it is derived |
| --- | --- |
| **minimal** | highest matching rate × the advertiser's **CPM floor adjustment** ÷ 100. The adjustment is a per-advertiser percentage that defaults to 100, so by default minimal equals the raw rate. It has no console control; set it over REST with the advertiser's `selfServeCpmFloorAdjustment` parameter (0–10000). |
| **suggested** | `(rate × A ÷ 100) + (rate × B ÷ 100 × (1 + rate)^-0.6)`, rounded to two decimals. A (default 207) and B (default 90) are server-side settings; ask Epom support or your account manager to change them. The advertiser adjustment does **not** enter this figure. |

With stock configuration a 1.00 rate yields a suggested CPM of 2.66 and, for an advertiser
left at 100%, a minimal of 1.00.

`GET /rest-api/floorCPMs` returns the raw rows instead, for a client that wants to render
the matrix itself; it **requires** `campaignType` and `os` (a missing or unknown one is a 400
naming it), and `region` or `geo` narrow it. Both endpoints use REST API authentication.

`suggest` needs `campaignType` and `advertiser` and answers
`{"SuggestedCPM": {"minimal": …, "suggested": …}}`. It is **403** without a view share on the
advertiser, and **404** when the advertiser does not exist or no rate matches. `os` takes names
or IDs, comma-separated; a `geo` value that starts with `-` (`geo=-US`) excludes those
countries instead of selecting them, and `*` / `0` select the Global row.

#### The floor is advice, not a gate

Nothing in the ad server ever compares a bid against these rates. They are read only by the
admin pages here and the two REST endpoints above. The floors that ad serving does apply are
the unrelated publisher **bid floor** and the OpenRTB deal floor.

The advertiser's CPM floor adjustment plays no part in ad serving either. If your storefront
lets a buyer price below the suggested figure, the campaign serves at that price.

## Options and fields

### Prototype banner form

| Field | Required | Notes |
| --- | --- | --- |
| **Self-Serve type** | Yes | One of the seven types. Defaults to Banner (desktop). Only one prototype may exist per type. |
| **Prototype banner ID** | Yes | Numeric ID of the seed banner. It must not be deleted, and neither its campaign nor its advertiser may be deleted. Left empty, the form refuses to submit with *Banner ID is required*; an ID the server cannot resolve comes back as *Banner not found.* under the field. |
| **Default conversion action ID** | No | Numeric ID of a conversion action that is not deleted and not shared by default. Clear the field to unset it. An ID that does not resolve is refused with *Action with id [N] is not found* under the field — see the corner case below. |

### Floor matrix controls

| Control | What it does |
| --- | --- |
| **Upload** | Multipart CSV upload into the campaign type you are editing. The only way to create rows. |
| Cell (inline edit) | Updates one existing country/OS rate. Enter or blur commits; Escape reverts. |
| Row checkbox / header checkbox | Selects one country, or all of them. |
| **Delete** | Removes the selected countries' rates for this campaign type. Disabled until something is selected. |

## Corner cases and gotchas

> **Corner case:** a **Default conversion action ID** that does not resolve — no such action, a
> deleted one, or one that is shared by default — is refused with *Action with id [N] is not
> found* on that field, and nothing is saved. Because the field is optional and stored but
> never applied (see the warning above), still check the grid after saving: the ID is only
> really set if it appears as `[id: N] name`.

> **Corner case:** the two ways of writing a rate do not overlap. CSV upload is the only way
> to **create** a country's rates; inline editing is the only way to **change** one. An
> inline edit on a country/OS with no rate behind it is rejected with *No corresponding
> record found.*

> **Corner case:** re-uploading a corrected price file does nothing. Every line whose campaign
> type, OS and country already exist is discarded in silence — and unlike a malformed line this
> produces no error at all, just the
> green **Done**. To reprice a country, either edit its cells by hand or delete the country's
> row and upload it again.

> **Corner case:** because a country is only accepted with all five operating systems at
> once, every row you can see should already have five rates. A `—` in the grid means the
> row was written outside the admin UI, and clicking that cell will not fill it in — the
> update call has no record to update.

> **Corner case:** an OS spelled as its enum constant — `MAC_OS_X`, `IOS`, `WINDOWS` — does
> not resolve. The CSV parser matches display names, case-sensitively. A file written
> entirely in enum constants is rejected line by line (*Invalid line: [ZZ, WINDOWS, 1.10]*)
> and nothing is written; a file that uses them for only some rows loses those rows and then,
> for want of a complete set of five, loses **every country that touched them** as well. The
> rest of the platform is more relaxed about this spelling, so a value another endpoint
> accepts is no guarantee the CSV parser will.

> **Corner case:** `*` (**Global**) is not a fallback. `/rest-api/floorCPMs/suggest`
> filters by exactly the countries you pass, so `geo=US` with no US rows returns **404** —
> it does not fall back to the Global row. Global only participates when the caller passes
> `geo=*`, `geo=0`, or no `geo` at all. Seed the specific countries your storefront asks
> for.

> **Corner case:** the suggested CPM is derived from the **highest** matching rate, not an
> average and not the closest match. A single expensive country or OS left in an
> unconstrained query pulls the whole suggestion up.

> **Corner case:** a rate keeps four decimal places and cannot be negative; the largest rate it
> can hold is 9 999 999.9999. The inline editor and the CSV parser both reject values below zero.

> **Corner case:** a prototype's banner and default action cannot be hard-deleted while the
> prototype references them.
> A normal delete in the account UI is a *soft* delete, though, and passes straight through:
> the prototype row survives, looks healthy in the grid, and every clone request for that
> type starts failing with *Banner id not found.* Audit prototypes after pruning creatives.

> **Corner case:** the seven campaign types are a fixed list. There is no eighth, and adding
> one is a product change.

> **Corner case:** the two pages report a failure very differently. The prototype grid says
> **Failed to load**; the floor matrix says **No data available** — the same words an empty
> matrix uses, with no toast and no other signal. A matrix holding six countries of rates
> renders exactly like an empty one the moment its request fails, whether that is a missing
> permission or a dropped connection. If a matrix looks empty and you are not certain it
> should be, reload before concluding there are no rates.

> **Corner case:** the campaign-type list on the floor-rates page labels its second column
> **Conversion actions**. It holds the Edit links and has nothing to do with conversion
> actions. Ignore the heading.

> **Corner case:** the white-label `hideSignUp` property only hides the sign-up link on the
> login page; it does not disable `/signUp`. Which roles can sign up is each role's *Sign-up*
> grant — see [Configure white-label branding](https://epom.com/ad-server/docs/guides/admin/white-label.md).

## Per-role notes

- **Administrator** — needs each page's admin permission; without it the page still opens by
  URL but never loads.
- **Network manager, publisher, advertiser** — cannot reach these pages. The campaign's
  self-serve type and the advertiser's CPM floor adjustment are admin-only grants as well.
- **Self-serve user** — an ordinary network user. Whether a role may sign up is that role's
  *Sign-up* grant, not a white-label setting.

Exact grants: see [Manage users, roles and permissions](https://epom.com/ad-server/docs/guides/admin/users-and-roles.md).

## Related

- [Use case: run a white-label self-serve ad network](https://epom.com/ad-server/docs/use-cases/self-serve-reseller-network.md)
- [Manage users, roles and permissions](https://epom.com/ad-server/docs/guides/admin/users-and-roles.md)
- [Configure white-label branding](https://epom.com/ad-server/docs/guides/admin/white-label.md)
- [Admin area map](https://epom.com/ad-server/docs/guides/admin.md)