---
title: "OpenRTB integration reference"
description: "Field-level reference of both OpenRTB directions — the bid request Epom sends DSP partners and the response fields it reads back, the floor / PMP / creative-scan filters, the loss reasons and auction macros a buyer receives, and the /hb/bid endpoint Epom answers as a seller."
type: "reference"
audience: [network-manager, admin]
keywords: [OpenRTB, RTB, bid request, bid response, imp, banner, video, native, pmp, deals, bcat, schain, second price, bid floor, privateBrowsing, integration, DSP, adomain, loss reason, AUCTION_PRICE, AUCTION_LOSS, currency, USD, hb/bid, seller, seat]
---

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.

# OpenRTB integration reference

This is the field-level map for anyone plugging a DSP into Epom, or plugging Epom into a buyer, and
for debugging why a partner sees no bid opportunity, a malformed request, or a bid that never wins.

Epom speaks OpenRTB in **two directions**, and they are separate implementations with different
defaults:

| Direction | Epom's role | Endpoint | Protocol |
|---|---|---|---|
| **Outbound** — Epom asks DSPs to bid | Exchange / SSP | The partner's endpoint | Per partner, default **2.5** |
| **Inbound** — a buyer asks Epom to bid | Seller / bidder | `POST /hb/bid` on the ad-serving host | **2.6** |

Most of this page is the outbound direction, because that is where the field surface is large. The
inbound direction is covered under [Epom as a seller](#epom-as-a-seller-hbbid); the product side of
it lives in [Header bidding](https://epom.com/ad-server/docs/guides/inventory/header-bidding.md).

For the outbound direction: for each OpenRTB banner reached during
[ad selection](https://epom.com/ad-server/docs/concepts/how-ad-selection-works.md), Epom builds **one bid request per eligible
banner**, POSTs it to that banner's DSP partner endpoint, and runs an auction over the returned
bids. Partners are configured in [DSP Partners](https://epom.com/ad-server/docs/guides/admin/dsp-partners.md); the demand-side
banner in [OpenRTB banners](https://epom.com/ad-server/docs/guides/advertising/banner-openrtb-dsp.md).

> **Note:** two banners pointing at the same partner produce **two** requests, not one — the unit
> is the banner, not the partner. A banner whose DSP partner is deactivated is dropped before any
> request is built.

> **Note:** the request is a JSON `POST` with `Content-Type: application/json; charset=UTF-8` and
> an `x-openrtb-version` header naming the partner's protocol version; the body is gzipped only
> when the partner has **Gzip bid request** on. Version, timeout, currency conversion and
> endpoint are per-partner settings — see the
> [DSP Partners field table](https://epom.com/ad-server/docs/guides/admin/dsp-partners.md). Protocol version defaults to
> **2.5**; native protocol to **1.1**. Null fields and empty arrays are omitted from the JSON
> rather than sent.

> **Note:** on a [header-bidding](https://epom.com/ad-server/docs/guides/inventory/header-bidding.md) call, a DSP partner is
> asked only when its **Allow in header bidding** setting is on, the platform-wide programmatic
> switch is on, and its pricing model is CPM. Otherwise its banners are skipped for that call.

## Currency: USD, always

**Epom is a single-currency exchange.** The SSP currency is a hard-coded constant, and it appears
on the wire in four places:

- the outgoing `cur` on every bid request defaults to `["USD"]`;
- `imp.bidfloor` and `pmp.deals[].bidfloorcur` are USD;
- the `${AUCTION_CURRENCY}` macro always substitutes the literal `USD`, whatever else is
  configured;
- Epom's own bid response as a seller declares `cur: "USD"`.

There is **no FX feed and no currency negotiation**. The only escape is a pair of per-banner
network parameters — **DSP Custom Currency** and **USD to DSP Custom Currency Rate** — which you
set together on the OpenRTB banner. When both are present, Epom sends `cur` and `imp.bidfloorcur`
as your currency with the floor converted at your rate, and converts the returned `bid.price` back
to USD at the same rate.

> **Warning:** that rate is a **static number you typed**, not a live exchange rate. It does not
> update, and nothing alerts you when it drifts. If the rate is missing or does not parse, Epom
> silently falls back to `1.0` — meaning a EUR bid is banked as though it were USD.

> **Corner case:** because Epom rewrites the response currency to `USD` during that conversion, the
> "reject a non-USD bid response" check that exists in the floor validator can never actually fire
> on the DSP path. Sending `cur: "EUR"` in your bid response therefore does **not** produce an
> error — the price is taken at face value (or at your configured rate). Getting the rate
> parameters right is the only protection.

## The bid floor

`imp.bidfloor` is the **first non-zero value** along this chain, evaluated for the request's
country:

**banner Bid Floor → placement Bid Floor → site Bid Floor → the banner's own price → the campaign
price → the site CPM threshold**

A header-bidding request raises the result to the publisher's floor when that is higher.

If every link is unset or zero, no floor is sent. An empty country code short-circuits the chain to
zero. The two price links only contribute when the pricing type yields an eCPM — see
[CPM threshold](https://epom.com/ad-server/docs/guides/delivery/cpm-threshold.md) for the pricing-type table.

> **Warning:** the javadoc above this method in the source states a **different** order (placement
> before banner) and lists a seventh step, "open rtb country bid floor", that does not exist. The
> order above is what the code does. Do not use the javadoc.

Two things sit outside the chain:

- **A header-bidding caller's floor raises ours.** When the inbound request names its own floor,
  the value sent onward to DSPs is the higher of the two. This applies only to header-bidding
  requests; ordinary serving keeps the chain's answer.
- **There is no per-partner floor.** Nothing on the DSP partner sets or adjusts a floor, and there
  is no way to quote different partners differently except by giving them different banners.

> **Note:** **nothing takes a margin off DSP spend.** The winning bid price is banked as the raw
> settlement price. Epom's margin is taken on the publisher side, as revenue share on the site or
> placement — the buyer's price is never marked down. (A "fee" field exists in the floor model but
> is never populated by any code path; it is always zero.)

## Request: top-level `BidRequest`

Eight fields ship on every request; four more are conditional.

| Field | What Epom sends | Always? |
|---|---|---|
| `id` | Generated UUID per request | Yes |
| `imp[]` | Exactly one impression | Yes |
| `site` **or** `app` | `app` when the site's inventory type is APP or the placement is an application placement; otherwise `site`. Sending neither or both is rejected before the request leaves. | Yes (one of) |
| `device` | See [device](#request-device) | Yes |
| `user` | See [user](#request-user) | Yes |
| `at` (auction type) | `1` (first price) or `2` (second price) — see below | Yes |
| `tmax` | The partner's configured **Timeout (ms)**, **minus** the time already spent on this auction batch | Yes |
| `cur` | `["USD"]`, or the partner's custom currency | Yes |
| `bcat` | Blocked IAB categories — union of blocked categories on **site + zone + placement** | When non-empty |
| `source` | `pchain`, plus `schain` — 2.5 puts it in `source.ext.schain`, 2.6 in `source.schain` | Only with the *Add source schain* network parameter |
| `test` | `0` | DoubleImpact only |
| `ext` | `ext.sub = placementId` | ExoClick only |

> **Corner case:** `at` is **not** always first price. It is `2` (second price) when the banner has
> *Always 2nd price* set and it is the only programmatic candidate, and also when several
> programmatic banners compete and the partner's `FORCE_FIRST_PRICE` parameter is explicitly set to
> `false`. Note the inverted sense of that parameter: **absent** and **`true`** both mean first
> price; only an explicit `false` opens the door to second price. See
> [First vs second price](https://epom.com/ad-server/docs/concepts/auction-first-vs-second-price.md).

> **Corner case:** the `tmax` you receive is the Timeout configured on the partner **minus** the
> time the current auction batch has already used, so it is usually smaller than the configured value. If
> 10 ms or less remains, the request is abandoned and never sent at all.

**Not sent:** `badv`, `bapp`, `wseat`, `allimps`, and `regs` — the `regs` object does not exist on
Epom's outgoing model at all, so there is no `regs.gdpr`, `regs.us_privacy`, `regs.gpp` or `coppa`.

## Request: `imp` by placement type

Every request carries exactly one impression object, typed to the placement.

### Display (`imp.banner`)

| Field | Value |
|---|---|
| `banner.id` | The placement id |
| `banner.w` / `banner.h` | The banner's own size, or the placement's size when **Use placement size** is on |
| `banner.format[]` | A single `{w,h}` format — **2.5 partners only** |
| `banner.pos` | The placement's ad position |
| `banner.battr` | Blocked creative attributes from the placement |

> **Corner case:** `format[]` is sent to **2.5 partners only**, and never to 2.6 partners — which
> is the reverse of what the spec's own direction of travel suggests, since 2.6 is the version that
> deprecates `banner.w`/`h` in favour of `format[]`. A 2.6 partner receives `w`/`h` and no
> `format`.

> **Corner case:** `banner.btype` is **never populated**. Only `battr` carries the placement's
> blocking. `banner.mimes`, `banner.expdir` and `banner.api` are likewise never sent.

> **Corner case:** on pop placements, several partner types receive **no banner object at all**
> (it is set to null), and DoubleImpact receives an empty one.

### Video (`imp.video`)

| Field | Value |
|---|---|
| `video.linearity` | Linear / non-linear, from the banner's In-Stream video type |
| `video.mimes[]` | The banner's **Mime types** list, or Epom's default video mime set when empty |
| `video.minduration` / `video.maxduration` | The placement's min/max ad duration |
| `video.protocols[]` | A fixed list — see below |
| `video.skip` / `video.skipmin` / `video.skipafter` | From the placement's skip settings |
| `video.playbackmethod[]` / `video.api[]` | The placement's RTB playback methods and API frameworks, when non-empty |
| `video.w` / `video.h` | The placement ad-unit size, when set |
| `video.battr` | Blocked video attributes from the placement |
| `video.minbitrate` / `video.maxbitrate` | From the VAST request, when present |
| `video.pos` / `video.sequence` / `video.maxextended` / `video.boxingallowed` | Always sent: `0` / `1` / `0` / `1` by default |
| `imp.ssai` | `3` (stitched and tracked server-side) when the placement's **Server-side ad insertion (SSAI)** setting names a stitcher; omitted otherwise — see [Video placements](https://epom.com/ad-server/docs/guides/inventory/placement-video.md#tell-epom-which-stitcher-calls-the-tag) |

`video.protocols[]` is a **hard-coded six-element list** that does not vary by placement, partner or
protocol version:

| Protocol | Id |
|---|---|
| VAST 2.0 | 2 |
| VAST 3.0 | 3 |
| VAST 2.0 wrapper | 5 |
| VAST 3.0 wrapper | 6 |
| VAST 4.0 | 7 |
| VAST 4.0 wrapper | 8 |

> **Corner case:** VAST 1.0 (1, 4) and VAST 4.1 / 4.2 / 4.3 (11–16) are all modelled in Epom's enum
> but are **deliberately never advertised**. A DSP that only bids on 4.2 will never see a matching
> protocol, and there is no setting that adds one.

> **Corner case:** `boxingallowed` defaults to `1` and therefore ships on every video impression,
> not only when a VAST request supplies it. The VAST request only *overrides* the default.

### Native (`imp.native`)

| Field | Value |
|---|---|
| `native.ver` | The partner's native protocol version (default 1.1) |
| `native.request` | The native request spec built from the placement, optionally wrapped per the partner's *native request wrap* setting |
| `native.battr` | Blocked native attributes from the placement |

`native.api` and `native.ext` are never sent.

### Shared `imp` fields (all types)

| Field | Value |
|---|---|
| `imp.id` | The request's **transaction id** — not the placement id |
| `imp.tagid` | The placement key, or a custom OpenRTB tag id resolved from the placement (macros expanded) |
| `imp.bidfloor` | The calculated floor — see [The bid floor](#the-bid-floor) |
| `imp.bidfloorcur` | `USD`, or the partner's custom currency |
| `imp.instl` | `1` for interstitial **and** rewarded placements, or when the partner forces interstitial |
| `imp.rwdd` | `1` when the placement's **Application ad category** is *Rewarded video*; omitted otherwise. A rewarded placement carries `imp.instl = 1` as well, because it is full screen too — `rwdd` is the only field that tells the two apart, so a buyer that wants rewarded inventory (or wants to avoid it) reads this one |
| `imp.secure` | `1` on HTTPS requests |
| `imp.pmp` | Present only when the campaign has [PMP deals](#request-pmpdeals) |
| `imp.ext` | Partner-specific (e.g. TrafficHunt `subid`, Epom WL DSP `partnerRef`); TwinRed always gets `adm_on_resp: 1`. On an always-redirect placement a pop is `type: "pop"` for Custom partners, and for TwinRed the impression is sent without an `imp.banner` object — see [pops](https://epom.com/ad-server/docs/guides/inventory/pop-under-direct-link.md#buying-pops-from-an-openrtb-partner) |

`imp.displaymanager`, `imp.displaymanagerver`, `imp.exp` and `imp.clickbrowser` do not exist on
Epom's model and are never sent.

## Request: `site` and `app` — page URL, domain and publisher

The request carries `site` for web traffic and `app` for app inventory (see the top-level table).

| Field | `site` (web) | `app` |
|---|---|---|
| `domain` / `name` | Host of the page that called the tag: the page origin (the tag's `o` parameter or the `Origin` header), else the `Referer` host, else the URL configured on the site | `app.name` is the site's name; `app.domain` only when the SDK request passes one |
| `page` | The full page URL — the tag's `requestUrl`, else the `Referer` — when it belongs to that origin | — |
| `ref` | The document referrer (the tag's `requestRef`), when known | — |
| `id` | A numeric hash of the domain; a custom parameter `ortb.site.id` (`cp.ortb.site.id` in the tag) replaces it | The site id |
| `bundle` / `storeurl` / `ver` | — | The site's **Bundle** setting; the SDK request's `bundle` (or the WebView's `X-Requested-With` package), `storeurl` and `ver` override it |
| `publisher.id` | A generated publisher id per site — never the account | Same |
| `cat` / `sectioncat` / `pagecat` | IAB categories of the site, the zone and the placement | Same |
| `privacypolicy` | The site's privacy-policy flag | — |

### The buyer sees a Google domain (imasdk.googleapis.com) instead of the site

Inside Google's IMA player the tag is requested from Google's SDK frame. Epom drops `imasdk.googleapis.com` from the
origin and referrer and falls back to the site URL configured in Epom, so set the site's URL. A
page that sends no `Referer` (a strict referrer policy, some players) also leaves `page` empty —
pass the page URL in the tag's `requestUrl` parameter (`data-page` on the async tag), which
replaces the Referer, and `requestRef` for the document referrer.

### Readable source id or zone name in the partner’s stats

`imp.tagid` carries the placement key by default. To show the buyer a readable id (for example `sports-daily-top`), set the placement's **Custom tag ID** on
its RTB configuration card — see [OpenRTB placement settings](https://epom.com/ad-server/docs/guides/inventory/placement-openrtb.md).

### Is pre-roll, mid-roll or post-roll sent to the DSP?

No. Outbound video requests carry no `video.startdelay`, so a DSP cannot tell a pre-roll from a mid-roll or post-roll.

## Request: `device`

| Field | Value | Notes |
|---|---|---|
| `ua` | User agent | Always |
| `ip` / `ipv6` | Client IP | One or the other by address family |
| `js` | Always `1` | Always |
| `connectiontype` | Mapped to the OpenRTB connection-type enum | When known |
| `carrier` | Mobile carrier | When known |
| `language` | Browser language | When known |
| `os` / `osv` | Operating system + version | When detected |
| `devicetype` | PHONE / TABLET / CONNECTED_TV / PC | Derived from OS device format |
| `make` / `model` / `hwv` | Vendor / family / model | From device detection |
| `ifa` / `dpidsha1` | Advertising IDs | The advertising ID (`device.ifa`) and its SHA1 platform-ID form (`device.dpidsha1`), from the request's `ifa` parameter or the Android/iOS advertising-ID identification. An all-zero ID, an unsubstituted macro (`[IFA]`) or a placeholder such as `null` / `unknown` is treated as no ID |
| `lmt` | Limit ad tracking, `1` / `0` | From the request's `lmt` parameter; omitted when absent |
| `ext.ifa_type` | Source of `ifa` — `rida`, `tifa`, `aaid`, `sessionid`, … | From the request's `ifa_type` parameter, lower-cased; sent only alongside an `ifa` |
| `geo` | See below | May be absent entirely |
| `ext.timezone` / `ext.timezoneCountry` / `ext.releaseYear` | Non-standard extensions | Timezone name, its primary country (alpha-3), device release year |

`device.geo` carries `country` (ISO **alpha-3**), `type`, `utcoffset`, and `lat`/`lon`.

> **Corner case:** `geo.lat`/`lon` are **not GPS-only**. They are filled from IP geolocation on an
> ordinary request, with `geo.type = IP`. Coordinates passed as request parameters are a second
> source and flip `geo.type` to `GPS`. Read `geo.type` if the distinction matters to you — a
> precise-looking lat/lon may be city-level IP data.

> **Corner case:** the whole `geo` object is **omitted** when neither an alpha-3 country nor GPS
> parameters could be resolved. `geo.region`, `metro`, `city` and `zip` are never sent.

> **Corner case:** Epom does **not** populate `device.dnt`, and sends `device.lmt` only when the
> ad request carried an `lmt` parameter (CTV tags behind a stitcher do). Otherwise a DSP filtering
> on them sees them absent (treated as unknown), not as `0`. Epom forwards `lmt = 1` as it is and
> does not withhold the ID: a device that limits tracking already zeroes or session-scopes it.

> **Corner case:** screen width and height are **not** forwarded. Epom collects them (they reach
> the ad log and the creative-scan context) but drops them before the bid request, so
> `device.w`/`h` never appear. `ppi`, `pxratio`, `sua` and `mccmnc` do not exist on the model.

## Request: `user`

| Field | Value | Notes |
|---|---|---|
| `id` | Epom's user id (UID) | Always |
| `buyeruid` | The partner's synced user id | Only when **user sync** is enabled on the partner and its sync cookie is present |
| `customdata` | Partner-specific custom data | Keyed by the partner shortname |
| `keywords` | The request's `keywords` custom parameter | |
| `gender` / `yob` / `data[]` | DMP segments | Only when DMP-to-bid-request enrichment is enabled |
| `ext.privateBrowsing` | `1` (incognito) / `0` (normal) | Omitted when the state is unknown |
| `ext.xrw` | The in-app WebView package name | From the `X-Requested-With` header |
| `ext.channel` | The request channel | |
| `ext.category` / `ext.income` | DMP-derived audience attributes | Only with DMP enrichment |
| `ext.<custom-param>` | Any custom parameter on the request | Macros in the values are resolved before sending |

> **Note:** `user.ext.privateBrowsing` is a **proprietary Epom extension**, not an OpenRTB field.
> No standard field for private-browsing state exists in 2.5, 2.6 or 3.0, so Epom follows the
> `dnt`/`lmt` convention with an integer `1`/`0` and omits the key when the state is unknown. Do
> not expect other exchanges to send it.

> **Corner case:** GDPR consent is **not** sent as a standard OpenRTB field. Epom carries the IAB
> consent string internally and uses it to gate its own serving and to fill consent macros in
> markup — but the outbound bid request has **no `user.consent` and no `regs.ext.gdpr`**, because
> neither field exists on the outgoing model. The supported workaround is to pass the consent
> string as a **custom parameter**, which arrives as `user.ext.<your-param-name>`; agree the name
> between the network and the DSP.

## Request: `pmp.deals`

Present only when the winning banner's **campaign** has PMP deals enabled and at least one deal
matches. Set on `imp.pmp`:

| Field | Value |
|---|---|
| `pmp.private_auction` | `1` when the campaign marks the auction private, else `0` |
| `pmp.deals[].id` | The deal id |
| `pmp.deals[].bidfloor` | The deal's floor |
| `pmp.deals[].bidfloorcur` | `USD` |
| `pmp.deals[].wseat[]` | The deal's buyer (seat) id — always a single-element array |
| `pmp.deals[].at` | The deal's auction type |

Deals are matched by the request country first, then any **global** (country-agnostic) deals are
appended, de-duplicated. A private auction (`private_auction = 1`) with an empty deal list is
rejected before the request is sent — no HTTP call is made. A **non**-private auction with an empty
deal list simply drops the `pmp` object and sends the request without it.

## Inventory masking

If the banner's mask-inventory setting (or the site's) applies, `site.name` becomes `Site_<id>`
and `site.domain` / `site.page` / `site.ref` are cleared. With field-hashing enabled, name and
domain are sent as MD5 hashes instead. Publishers are always sent as a **generated publisher id**,
never the raw account; the same id is used in `pchain` and `schain.sid`.

> **Note:** even **unmasked**, `site.name`, `site.domain` and `site.id` describe the **live page
> domain**, derived from the ancestor-origin or referer — not the Epom site entity's name. `site.id`
> is a hash of that domain. Do not expect it to match anything in the Epom UI. Only `http(s)`
> addresses count: an origin or referer such as `about:blank`, `data:…` or `android-app://…` is
> ignored, and when neither is usable the site's configured URL supplies the domain instead.

## Response: fields Epom reads

Epom parses the standard `seatbid[].bid[]` structure. Anything not listed here is **discarded on
parse** — the deserializer ignores unknown fields.

| Response field | Used for |
|---|---|
| `price` | Auction ranking, clearing price, billing |
| `adm` | The served creative (HTML / VAST XML / native JSON) |
| `nurl` | Win notice; for some partners the markup is delivered here |
| `lurl` | Loss notice — see [Loss reasons](#loss-reasons) |
| `impid` | Matched against the request's impression; a bid that matches none is dropped |
| `crid` | Creative-scan verdict key, troubleshooting, reporting |
| `adid` | Fills the `${AUCTION_AD_ID}` macro |
| `adomain[]` | Carried onto the winning creative and republished as the `adomain` of Epom's own bid when that creative is resold through [header bidding](https://epom.com/ad-server/docs/guides/inventory/header-bidding.md) — unless the advertiser declares its own **Advertiser domain**, which wins |
| `dealid` | Validated against the request's deals |
| `ext` | `ext.url` and `ext.impression_tracking_url`, for Epom WL DSP pops |
| `seatbid[].seat` | Deal `wseat` validation, loss records |
| Response `id` / `bidid` / `cur` / `customdata` | Auction macros and currency handling |

> **Corner case:** several fields you might expect to matter are **parsed and then ignored**:
> `cid`, `attr[]` and `iurl` have no consumer anywhere in the serving path, so a campaign id sent on
> the bid does **not** reach Epom's reporting. And `w`/`h` are not even modelled — creative size is
> taken from the *request's* impression, never from your bid. Send them for spec compliance, but do
> not rely on Epom acting on them.

> **Warning:** Epom accepts **exactly one seatbid containing exactly one bid**. A response with two
> seat objects, or one seat carrying two bids, is discarded **whole** as an invalid bid response —
> not trimmed to the best bid. If your bidder normally returns a bid per impression or several
> candidates per seat, it must be configured to return a single bid to Epom.

## What happens to a returned bid

A bid passes through four stages before it can serve. They run in this order:

**1. Response validation and floor.** The response must be exactly one seatbid with one bid, and
the price must be at or above `imp.bidfloor`. A bid below the floor is dropped with
`BID_WAS_BELOW_AUCTION_FLOOR`.

> **Corner case:** a bid carrying a `dealid` **skips the floor check entirely**. Deal bids are held
> to the deal's own floor instead, at stage 3. A deal priced below your open-auction floor still
> clears.

**2. Private-auction pre-filter.** When any impression in play is a private auction, seat bids
whose deals are unsuitable are removed, and a partner left with nothing is dropped.

**3. Creative scan.** If the network scans the partner's creatives
([creative scanning](https://epom.com/ad-server/docs/use-cases/creative-policy-protection.md)), the bid's creative is checked
*before* the winner is chosen. A **BLOCKED** creative is removed from the auction, so the
runner-up wins instead — the impression still fills, just not with the blocked creative. Bids
removed here are recorded as `CREATIVE_FILTERED_GENERAL_REASON_UNKNOWN`.

**4. Deal validation, inside the auction.** This runs whenever **any** bid carries a `dealid`,
private auction or not:

- a bid whose `dealid` + `seat` matches no deal on the impression is dropped (`INVALID_DEAL_ID`);
- a bid below its deal's floor is dropped (`BID_WAS_BELOW_DEAL_FLOOR`);
- a **Bid floor price** deal has the bid's price **overwritten with the deal floor**, whatever was
  bid;
- once any private-auction bid is present, every non-private bid is removed.

> **Corner case:** a bid with **no** `dealid` is not dropped by stage 4 — it competes normally
> unless a private-auction bid displaces it. The page previously implied otherwise.

> **Is there a size limit on a bid's markup or video?** Not in Epom: the ad server sets no cap
> on the bid response or on `adm`, and a VAST in `adm` points at its video files by URL, so the
> file size never passes through Epom. A size limit quoted for a DSP's video bids comes from the DSP,
> the player or the upload limit for your own video banners — not from the auction.

### Native bids: which `imptrackers` and `eventtrackers` fire as impression beacons

For a native bid, Epom takes the impression trackers out of the native response in `adm` and
fires them as **impression beacons** of the served ad, next to its own:

| Native response version | Trackers fired at the impression |
|---|---|
| 1.0, 1.1 | Every URL in `imptrackers` |
| 1.2 | Every URL in `imptrackers` **plus** every `eventtrackers` entry with `event: 1` (impression) and `method: 1` (image pixel) that has a `url` — duplicates fired once |

Not fired: `eventtrackers` with `method: 2` (JavaScript), entries for other events (viewability
`2`–`4` and custom events), and `jstracker`. Each tracker URL is macro-processed first, so
`${AUCTION_PRICE}` and similar auction macros are filled. In a native template the beacons are
written as hidden 1×1 images by `$$NATIVE_IMPRESSIONS$$`, so they load when the ad renders — the
same moment Epom's own impression beacon fires. They do not change how Epom counts: the
impression in Epom's reports is Epom's own beacon; the partner's trackers feed the partner's
numbers.

## Auction and clearing

Epom runs the auction over all surviving bids across all partners for the impression. Second-price
clearing applies only when the auction type resolved to second price **and more than one bid
survived**; the clearing price is then:

`min( max(deal floor + $0.01, second-highest bid + $0.01), winning bid )`

— so it never exceeds what the winner actually bid, and never drops below a deal's negotiated
minimum. With a single surviving bid the winner clears at its own price, capped toward the floor
plus a cent when the banner is set to *Always 2nd price*. One cent is `0.01` USD.

The clearing price is what feeds the served markup's price macro and the impression's revenue — not
necessarily the raw `price` the winner sent. See
[First vs second price](https://epom.com/ad-server/docs/concepts/auction-first-vs-second-price.md) for the full model.

> **Corner case:** the DSP partner's **Second price only** checkbox (and the matching banner-level
> network parameter) does *not* change the clearing rule. It **withdraws the banner from the
> auction when it would be the only bidder** — the partner is saying "only bid me when there is
> competition". A partner that mysteriously never wins on low-competition inventory usually has
> this set.

## Loss reasons

Epom fills `${AUCTION_LOSS}` in your `lurl` with the **numeric** OpenRTB loss-reason code. Nine of
the specification's thirty codes are reachable:

| Code | Name | Why Epom sends it |
|---|---|---|
| 0 | `BID_WON` | You won |
| 3 | `INVALID_BID_RESPONSE` | Not exactly one seatbid with one bid, or the bid's `impid` matched no impression |
| 4 | `INVALID_DEAL_ID` | `dealid` + `seat` matched no deal on the impression |
| 7 | `MISSING_MARKUP` | The winning bid produced no usable markup |
| 100 | `BID_WAS_BELOW_AUCTION_FLOOR` | Price under `imp.bidfloor` (non-deal bids only) |
| 101 | `BID_WAS_BELOW_DEAL_FLOOR` | Price under the matched deal's floor |
| 102 | `LOST_TO_HIGHER_BID` | Ordinary auction loss |
| 103 | `LOST_TO_A_BID_FOR_A_PMP_DEAL` | Lost to a deal bid |
| 200 | `CREATIVE_FILTERED_GENERAL_REASON_UNKNOWN` | Removed by the creative scan |

> **Note:** `200` is the only signal you get for a creative-scan removal — the loss code does not
> say why the creative was removed.

Loss notices are sent only when the partner's **Call loss notification URL** parameter is enabled.

## Auction macros

Epom substitutes these in your `nurl`, `lurl` and `adm` before firing or serving them. An
unresolvable macro is replaced with an empty string, never left in place.

| Macro | Value |
|---|---|
| `${AUCTION_ID}` | The bid request `id` |
| `${AUCTION_BID_ID}` | The response `bidid` |
| `${AUCTION_IMP_ID}` | The `impid` of the impression won |
| `${AUCTION_SEAT_ID}` | The winning seat |
| `${AUCTION_AD_ID}` | The bid's `adid` |
| `${AUCTION_PRICE}` | The **clearing** price, not your bid |
| `${AUCTION_PRICE:$$ENC$$}` | The clearing price, encrypted per the partner's encryption setting |
| `${AUCTION_CURRENCY}` | Always the literal `USD` |
| `${AUCTION_CURRENCY:$$ENC$$}` | Encrypted form of the same |
| `${AUCTION_LOSS}` | The numeric loss reason — see above |
| `${AUCTION_MBR}` | Market bid ratio, **clearing price ÷ bid price** as OpenRTB defines it: 1 under first price, below 1 when a second-price auction clears under the bid |

> **Note:** the value is clearing ÷ bid, never the reciprocal, so it is at most 1. Do not invert it.

## Epom as a seller: `/hb/bid`

Epom also **answers** OpenRTB bid requests, as a bidder in a Prebid auction. This is the header
bidding path; the operational side — tags, slots, Prebid Server, reporting — is documented in
[Header bidding](https://epom.com/ad-server/docs/guides/inventory/header-bidding.md). The wire contract:

| | |
|---|---|
| Endpoint | `POST /hb/bid` on the ad-serving host. Any other method answers `405`. |
| Protocol | OpenRTB **2.6** |
| Request | `id` and at least one `imp` required; `site` or `app` required. Up to **40** impressions per request (configurable per network by Epom). |
| Validation | Cheap field checks only, not full JSON-Schema. A rejection is a `400` with a JSON `{"error": "<reason>"}` body naming the reason. |
| No bid | `204 No Content` — Epom does not send an OpenRTB no-bid object. |
| Response | One seatbid, seat `epom`, `cur: "USD"`. |

Each returned bid carries `id`, `impid`, `price`, `adm`, `crid`, `cid`, `w`, `h`, `mtype`
(`1` banner / `2` video / `4` native), `exp`, `adomain` when known, and `dealid: "epom-direct"` on
bought demand.

> **Note:** `adomain` is the **Advertiser domain** declared on the advertiser when the advertiser has one,
> otherwise the `adomain` that came in on the DSP bid the creative was bought from. When neither
> exists the field is omitted — an absent `adomain` is deliberate, not a bug.

> **Corner case:** `bid.exp` tells you how long the bid may sit in your cache, and it is derived
> from the impression beacon's own validity minus a few seconds of travel time. Render later than
> that and **the impression is never counted** — the bid is still valid markup, but you will not be
> paid for it.

> **Warning:** a `/hb/bid` call is a *bid*, not a served impression, but Epom does not yet treat it
> as one: selection still increments opportunity limits, request capping and pacing for bids that
> never win or render. Expect header-bidding traffic to consume capping and limit budget ahead of
> actual delivery, and read the [header bidding](https://epom.com/ad-server/docs/guides/inventory/header-bidding.md) reporting
> notes before reconciling numbers.

> **Corner case:** requests for a placement that no active header-bidding tag lists are refused by
> default. That gate has three settings and its own diagnostics — see
> [Header bidding](https://epom.com/ad-server/docs/guides/inventory/header-bidding.md).

> **Corner case:** on a video slot, `bid.adm` is a **VAST document Epom builds**, and for a VPAID
> framework creative its `<Duration>` can legitimately be `00:00:00`. VAST requires a Duration on
> every Linear, but a VPAID creative's real length is only known once the unit runs, so Epom emits
> a placeholder unless the framework declares a parameter literally named `duration` that parses as
> a positive number of seconds. A real duration always carries milliseconds (`00:00:15.000`); the
> bare `00:00:00` is the placeholder. Do not reject the creative on it, and do not use it to size a
> player or a timeout.

## Partner "network parameters"

Beyond the OpenRTB fields above, a partner integration uses named parameters drawn from a fixed
set of **18**. These are partner-onboarding values, filled on the DSP partner and/or the OpenRTB
banner — not OpenRTB spec fields. Sixteen are the checkboxes of the partner's **Parameters** card:
*Template Content Adapter*, *Broker Url*, *Win Notification URL*, *Traffic Tier*, *Mask
Inventory*, *Second Price Only*, *Allow empty DSP UUID*, *DSP Custom Currency*, *USD to DSP
Custom Currency Rate*, *Force First Price Auction Type*, *Call loss notification URL*, *Send
winner price in loss notification*, *Hash Publisher Site Field's Values*, *Add Source Chain to
Bid Request*, *Custom Source Supply Chain ASI* and *Interstitial*. The other two are the template
ids the code reads, *Template Native Adapter* and the deprecated *Template General*. No other
keys are accepted.

None of them is a fee, margin, commission or revenue share; no such parameter exists.

## Coverage note

This page documents both directions of the standard path in full: every field Epom's builder
populates, every response field it consumes, the filter pipeline, the loss reasons, the auction
macros, and the seller endpoint's contract.

This page does **not** enumerate **per-partner-type differences**. Several of the 16 branded
partner types receive small partner-specific adjustments — a nulled or emptied `banner` object on
pops, partner-specific `imp.ext` keys, forced interstitial, `test:0`, or the markup read from a
partner-specific place in the response. They change when a partner's integration is revised; ask
Epom support or your account manager what your specific partner type sends.

Two limits that are *not* gaps but firm behaviour, restated so they are not mistaken for omissions:
**no `dnt`** and **no standard consent or `regs`** on the outbound request — neither exists on
Epom's outgoing model. `device.lmt` is not in that category: it is sent whenever the ad request
carried an `lmt` parameter — see [Request: `device`](#request-device).

## Related

- [OpenRTB (DSP) banners](https://epom.com/ad-server/docs/guides/advertising/banner-openrtb-dsp.md)
- [Configure DSP partners](https://epom.com/ad-server/docs/guides/admin/dsp-partners.md)
- [First vs second price](https://epom.com/ad-server/docs/concepts/auction-first-vs-second-price.md) — the clearing model in full
- [CPM threshold](https://epom.com/ad-server/docs/guides/delivery/cpm-threshold.md) — the floor cascade and pricing types
- [PMP deals](https://epom.com/ad-server/docs/guides/advertising/pmp-deals.md)

- [Header bidding](https://epom.com/ad-server/docs/guides/inventory/header-bidding.md) — the seller direction, end to end
- [How ad selection works](https://epom.com/ad-server/docs/concepts/how-ad-selection-works.md)
- [VAST vs VPAID explained](https://epom.com/ad-server/docs/concepts/video-vast-vpaid-explained.md)