What is OpenRTB? A complete guide for ad-tech developers

OpenRTB is the wire protocol that powers nearly every real-time bidding auction on the open web. If you are building or debugging anything that touches programmatic advertising, you need a working mental model of how it actually fits together.

What OpenRTB actually is

OpenRTB is a specification, maintained by the IAB Tech Lab, that defines how an SSP (supply-side platform) asks a DSP (demand-side platform) whether it wants to bid on a single impression. The spec covers the wire format of the request, the structure of the response, the macros a DSP can put inside a creative, and the notification URLs that fire afterwards.

In practice that means OpenRTB is a JSON schema with a long list of object definitions and a thick book of enumerations. A typical integration looks like this: the SSP sends an HTTP POST with a JSON body containing one BidRequest to a DSP endpoint. The DSP has somewhere between 80 and 300 milliseconds to respond with either a BidResponse containing one or more bids, or an empty body (HTTP 204 No Content) meaning "no bid." That is the whole protocol at the wire level. Everything else is detail — and the detail is where the work lives.

The point of OpenRTB is interoperability. Before it existed, every exchange invented its own bid request format, and DSPs had to maintain a separate integration per partner. OpenRTB collapsed that to one schema with vendor-specific extensions in ext blocks.

A short history: from RTB chaos to IAB standard

Real-time bidding existed before OpenRTB did. Google's AdX (then called DoubleClick Ad Exchange) launched RTB in 2009 with a proprietary protocol — Google still uses a Protocol Buffers variant of it today, separate from the OpenRTB JSON path. Right Media and a handful of other early exchanges each had their own bidder protocols. Integrating with three exchanges meant writing three different parsers, three different bidder agents, and three different sets of notification handlers.

The IAB published OpenRTB 1.0 in 2010 as an attempt to unify all of that. It was a fairly thin spec at first — banner only, basic targeting, and a handful of objects. Version 2.0 (2011) and 2.1 (2012) filled in the gaps. Version 2.2 added native ad support. Version 2.3 (2015) is the one where most of the modern shape solidified. Version 2.4 added richer audio. Version 2.5 (2016) introduced BidRequest.source.pchain and a handful of fraud-fighting fields. Version 2.6 (2022) brought in real video pod support and Imp.rwdd for rewarded video. Version 3.0 was a major restructure that — eight years after it was published — most of the industry still has not adopted in production.

The bid flow in 200 milliseconds

Walking through a real auction makes the moving parts much easier to see. Suppose a user loads a news page that has one banner slot above the fold.

  1. The page's ad tag (or header bidding wrapper) fires a request to the publisher's ad server.
  2. The ad server, if it has a programmatic line item eligible, delegates the impression to an SSP.
  3. The SSP looks up the demand partners (DSPs) configured for that publisher, geography and format. For each one it generates an OpenRTB BidRequest and POSTs it to the DSP's bidder endpoint.
  4. Each DSP has roughly the value in BidRequest.tmax — typically 100 to 200 ms — to evaluate the request, run any campaign filtering logic, look up audience segments, decide which advertiser would value this user most, compute a bid price, build the creative ADM (ad markup), and POST a BidResponse back.
  5. The SSP collects all the bids that returned in time. Late responses are discarded — the auction is already over.
  6. The SSP runs the auction (first- or second-price), picks a winner, applies any publisher floor, and returns the winner's adm (or a wrapping VAST tag) up the chain.
  7. The browser renders the creative. As it does, win/loss notification URLs fire: the winning DSP sees its nurl hit, losers may see a loss-notification (LURL). When the impression is actually billable, the burl fires.

All of this happens between the moment the page calls for an ad and the moment the creative paints. The total budget is usually under 500 ms end-to-end, and the network round-trip from SSP to DSP eats half of that.

Anatomy of a BidRequest

A BidRequest is a JSON object with a small number of top-level fields and several nested object arrays. The required fields, per the spec, are id and imp. Everything else is optional — though in practice exchanges send a lot more, because DSPs filter on it.

Here is a minimal but realistic OpenRTB 2.6 banner request, lightly edited for readability:

{
  "id": "a4e8b6f2-1c33-4d4a-9b8a-9b9c2d6f7e91",
  "imp": [
    {
      "id": "1",
      "banner": {
        "format": [
          { "w": 300, "h": 250 },
          { "w": 300, "h": 600 }
        ],
        "pos": 1,
        "api": [3, 5, 6, 7]
      },
      "tagid": "homepage_atf_rect",
      "bidfloor": 0.45,
      "bidfloorcur": "USD",
      "secure": 1,
      "ext": {
        "gpid": "/12345/homepage#atf_rect"
      }
    }
  ],
  "site": {
    "id": "8842",
    "domain": "example-news.com",
    "page": "https://example-news.com/world/article-123",
    "publisher": {
      "id": "1138",
      "name": "Example News Network"
    }
  },
  "device": {
    "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) AppleWebKit/...",
    "ip": "203.0.113.42",
    "geo": { "country": "USA", "region": "NY", "city": "New York" },
    "devicetype": 2,
    "os": "macOS",
    "language": "en"
  },
  "user": {
    "id": "u-abcdef0123",
    "buyeruid": "dsp-side-cookie-xyz"
  },
  "at": 1,
  "tmax": 120,
  "cur": ["USD"],
  "source": {
    "fd": 0,
    "tid": "a4e8b6f2-1c33-4d4a-9b8a-9b9c2d6f7e91",
    "ext": {
      "schain": {
        "ver": "1.0",
        "complete": 1,
        "nodes": [
          { "asi": "sspdomain.com", "sid": "1138", "hp": 1 }
        ]
      }
    }
  },
  "regs": {
    "gdpr": 0,
    "ext": { "gpp": "DBABMA~CPXxRfAPXxRfAAfKABENB..." }
  }
}

The objects that matter most:

Anatomy of a BidResponse

The DSP responds with a BidResponse — or with HTTP 204 if it has no bids. A response looks like this:

{
  "id": "a4e8b6f2-1c33-4d4a-9b8a-9b9c2d6f7e91",
  "bidid": "dsp-internal-bid-id-77821",
  "cur": "USD",
  "seatbid": [
    {
      "seat": "ACME-Advertiser-Co",
      "bid": [
        {
          "id": "bid-1",
          "impid": "1",
          "price": 1.42,
          "adid": "campaign-9981-creative-44",
          "nurl": "https://dsp.example.com/win?auc=${AUCTION_ID}&price=${AUCTION_PRICE}",
          "burl": "https://dsp.example.com/bill?auc=${AUCTION_ID}&price=${AUCTION_PRICE}",
          "lurl": "https://dsp.example.com/loss?auc=${AUCTION_ID}&reason=${AUCTION_LOSS}",
          "adm": "",
          "adomain": ["acme-advertiser.com"],
          "cid": "campaign-9981",
          "crid": "creative-44",
          "w": 300,
          "h": 250
        }
      ]
    }
  ]
}

Each bid must reference an impid that exists in the request. price is in the currency declared in cur. adm is the actual creative markup; for video, it would be a VAST XML string. adomain declares the advertiser landing page domain — SSPs use this for advertiser-level blocking, so getting it wrong gets bids filtered.

The notification URLs deserve their own treatment, which we cover in how ad-tech impression tracking works.

First-price (at:1) vs second-price (at:2) auctions

The BidRequest.at field tells the DSP which kind of auction it is bidding into. Two values are common in production:

ValueAuction typeWhat the winner pays
1First-priceTheir own bid price
2Second-price plusSecond-highest bid plus one cent (or floor, whichever is higher)
3Deal-basedThe price negotiated in the PMP deal

From around 2017 onwards the industry has moved almost entirely to first-price (at:1). The reason is that header bidding made second-price unstable: a publisher would receive several second-price bids in parallel and pick the highest one, but that bid had been shaded down inside the DSP's bid shading logic on the assumption that it would only pay the second price. The result was systematic underpayment to publishers and a series of high-profile scandals about hidden fees. Google made AdX first-price in 2019 and the rest of the industry followed within about eighteen months.

If you are writing a bidder today, assume at:1 by default and bid your true value (minus your margin). If you see at:2 in production, treat it as a legacy signal worth logging.

2.5 vs 2.6 vs 3.0 — what actually changed

If a partner sends you BidRequest with no version header (and they usually do not), you have to detect the version from the fields present. Knowing the differences helps both with that and with planning your own roadmap.

2.5 (2016)

The last "small change" release. Introduced BidRequest.source.pchain (later largely replaced by schain), the metric object on Imp for sending viewability signals, and the banner.format array as a cleaner replacement for the older w/h/wmax/hmax pattern. 2.5 is still the version most exchange partners default to as of 2026 — partly because the changes in later versions are not strictly backwards compatible.

2.6 (2022)

A meaningful upgrade focused on video and connected TV. The major additions:

3.0 (2018)

The version most of the industry quietly skipped. 3.0 split the protocol into OpenRTB 3.0 (the auction layer) and AdCOM 1.0 (a separate spec covering creative and inventory metadata). The wire format became Protocol Buffers as well as JSON. It was technically cleaner — but adopting it meant rewriting every integration and gaining nothing the publisher could see. Eight years on, the 2.x line is still where production traffic flows, and the 2.6 release in 2022 was the IAB's tacit acknowledgement of that.

Timeouts: tmax, network budget, and lost bids

The single most consequential field in the request is tmax. It is the total time the SSP will wait for a bid, measured from when it sends the request to when it expects to have parsed the response. That budget includes:

If the request says tmax: 120 and your DSP responds in 100 ms but the round-trip network is 50 ms, you are too late. Most production DSPs target a P99 bid-decision time of under 80 ms — and then deploy bidder pods in every region where they receive significant traffic to keep network RTT below 30 ms.

Watch out for SSPs that lie about tmax. Some apply an internal "real" tmax that is 20 to 40 ms lower than what they advertise, to give themselves slack to run the auction. Measure your win rate against your stated bid time and you will spot it quickly.

Banner, video and native — three creative shapes, one envelope

Each Imp contains exactly one of banner, video, audio, or native (in 2.6 it can contain more than one to declare a multi-format opportunity — the DSP picks which to bid on by setting bid.mtype).

Banner

The simplest case. The DSP returns HTML or a JavaScript snippet in bid.adm. banner.api is a list of supported APIs — 3 is MRAID 1, 5 is MRAID 2, 6 is MRAID 3, 7 is OMID-1 (Open Measurement). If the impression is in a sandboxed iframe, banner.api will be empty and the DSP should not try to use MRAID.

Video

The DSP returns VAST XML in bid.adm, or a URL to a VAST tag in bid.nurl with the special adm field empty. We cover the VAST side in detail in understanding VAST tags. Key fields on video: mimes (allowed video MIME types), protocols (allowed VAST versions, where 2 = VAST 2.0, 3 = VAST 3.0, 5 = VAST 4.0, 6 = VAST 4.1, 7 = VAST 4.2), minduration, maxduration, placement (1 = in-stream, 2 = in-banner, 3 = in-article, 4 = in-feed, 5 = interstitial), and linearity.

Native

Native ads are described by a separate IAB Native spec (the current version is 1.2). Inside OpenRTB, the request carries a Native.request field containing a JSON string (yes, a JSON string inside a JSON object — quote escaping bugs are extremely common here, and a good JSON beautifier or escape/unescape tool is worth having handy). The response's adm is itself stringified JSON conforming to the native response schema — title, description, image, click URL, impression trackers.

The ext field — the protocol's escape hatch

Almost every object in the spec has an ext field. The spec says it is for "non-IAB-defined extensions" and instructs implementations to put a vendor-specific key inside it (e.g. imp.ext.appnexus or imp.ext.rubicon) so that multiple vendors can coexist without colliding.

In reality, some of the most important fields in modern OpenRTB live in ext:

The lesson: never write a parser that ignores ext. Many critical signals will be in there.

Common implementation pitfalls

A non-exhaustive list of the bugs we have seen most often when reviewing OpenRTB integrations:

  1. Treating site and app as mutually present. Exactly one will be set per request. Code that does request.site.domain without a null check crashes on every mobile-app bid.
  2. Sending bid prices in the wrong currency. The DSP must respond in one of the currencies listed in BidRequest.cur. If cur is ["EUR"] and you bid in USD without converting, the SSP will reject the bid silently and you will wonder why your win rate dropped.
  3. Forgetting adomain. SSPs use adomain for advertiser-level blocking and category filtering. If you leave it empty or send a tracker domain instead of the actual landing page, you get filtered at scale.
  4. Bid floor below the SSP's hard floor. Many SSPs have a hidden floor that they apply on top of imp.bidfloor. Bidding just over bidfloor often loses; bidding 20% over it wins. This is part of why bid shading exists.
  5. Macro substitution in the wrong place. ${AUCTION_PRICE} in nurl is replaced by the SSP. ${AUCTION_PRICE} inside adm is also replaced — but only some SSPs do this, and only on win. If you put unsubstituted macros into your tracking URLs and they fire, the DSP analytics will record literal strings.
  6. Not honoring secure: 1. When imp.secure is 1, all resources in the creative must be HTTPS. An HTTP image in a secure context blocks the creative in every modern browser.
  7. Treating tmax as the bid-decision budget instead of the round-trip budget. See the section above. Use round-trip latency monitoring per SSP, not an internal timer.

Tools to make this easier

Several of the small utilities on this site exist because we got tired of doing the same thing by hand:

None of these are special and you can do all of them at the command line. The browser version is just faster when you need an answer in twenty seconds.

Closing thought

OpenRTB is one of those protocols that looks intimidating until you realise it is essentially a JSON shape with a long enumeration list and a culture of vendor-specific extensions. Once you can read a BidRequest end to end and predict roughly what a sensible BidResponse would look like, ninety percent of programmatic debugging becomes navigable. The other ten percent is creative behaviour, which is its own genre of pain — covered in creative debugging in programmatic advertising.


Related reading