Post

Anatomy of an OpenRTB Auction: The Request, the Bid, and What Happens Next

Read a worked OpenRTB request and response, trace the auction deadline and IDs, and separate a winning bid from creative delivery and billing.

Learning path · Lesson 5 of 16

AdTech from first principles

  1. The invisible auction
  2. From campaign budget to publisher payment
  3. Who decides which ad gets served?
  4. The millisecond handshake
  5. Anatomy of an OpenRTB auction You are here
  6. Campaign lifecycle inside a DSP
  7. Trusting the programmatic supply chain
  8. ML in the millisecond
  9. A real-time ad platform under a millisecond budget
  10. The data exhaust of a page load
  11. The post-cookie frontier
  12. Practical privacy on the web
  13. Learning the win curve
  14. From win curve to bidding
  15. Floor-price optimization
  16. What did the ad actually cause?
Anatomy of an OpenRTB Auction: The Request, the Bid, and What Happens Next

Priya opens Wanderlust Weekly’s article about weekend treks. Its selling system offers a 300 × 250 ad slot to buyers. Urban Hiker’s DSP returns a bid of ₹85 CPM, and a boot advertisement eventually appears beside the story.

Our earlier articles followed the companies making these decisions, the browser requests that deliver the ad, and the money and data ledgers. Between those explanations sits a concrete interface: what does the seller actually send, what does the buyer return, and how do both sides identify the result?

That interface is OpenRTB. We will read one exchange between an SSP/exchange and a DSP, then deliberately break a few assumptions. A second slot will expose why request IDs are insufficient. A late, higher bid will expose why price alone cannot determine the winner. Notification URLs will expose why an auction result is not a receipt for a delivered ad.

We use the IAB Tech Lab’s OpenRTB 2.6 specification. Its published versioning policy permits compatible additions within 2.6, so an integration must agree on supported fields and behavior, not merely match the version header. All payloads, domains, timings, and prices below are synthetic teaching examples.


1. A Protocol Boundary, Not the Whole Ad Stack

For this article, the exchange is the selling system running the auction, and the bidder is Urban Hiker’s DSP. One company may perform multiple roles. OpenRTB describes their messages; campaign setup, model training, publisher contracts, and invoice settlement remain outside this conversation.

The exchange sends a bid request using HTTP POST. In our JSON integration, the relevant headers are Content-Type: application/json and x-openrtb-version: 2.6, over HTTPS. A bid response with content returns HTTP 200. An intentional no-bid can return HTTP 204 with no body. These are the standard transport conventions for the exchange-to-bidder call; the browser’s ad-tag traffic is separate.

Before traffic begins, the partners need an integration agreement: accepted currencies, supported creative formats, required metadata, creative review, timeouts, notification support, and the billable-event definition. A field can be optional in the base specification and mandatory for a particular exchange’s business rules.

This matters when diagnosing failures. “Both sides speak OpenRTB” establishes, at most, compatible JSON parsers; buying the other side’s inventory also needs compatible interpretations of the messages.

Validation should also preserve forward compatibility. A new optional field need not make an otherwise usable request invalid; OpenRTB’s versioning rules call for graceful handling of unfamiliar fields and enumerated values, without guessing their meaning. If an unfamiliar value is essential to deciding whether a format is supported, the bidder needs a defined unsupported-case outcome. Conversely, required IDs, field types, and references still need validation. A response can parse as JSON while containing an impid absent from the request. “Valid JSON,” “valid protocol message,” and “eligible offer under this integration” are three different checks.


2. Read the Request from the Outside In

Here is the exchange’s request. We add a second placement, a 728 × 90 banner, to make the distinction between a request and an impression opportunity visible.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  "id": "auction-8f3a1c",
  "at": 1,
  "tmax": 120,
  "cur": ["INR"],
  "imp": [
    {
      "id": "slot-1",
      "banner": {"format": [{"w": 300, "h": 250}]},
      "bidfloor": 70.0,
      "bidfloorcur": "INR",
      "secure": 1
    },
    {
      "id": "slot-2",
      "banner": {"format": [{"w": 728, "h": 90}]},
      "bidfloor": 50.0,
      "bidfloorcur": "INR",
      "secure": 1
    }
  ],
  "site": {
    "domain": "wanderlust.example",
    "page": "https://wanderlust.example/weekend-treks",
    "publisher": {"id": "pub-42"}
  }
}

The outer object establishes the conversation. id identifies this request. at: 1 declares a first-price auction; omitting at invokes the specification’s default of 2, meaning second price plus. tmax: 120 sets the bid-response time allowance in milliseconds, including network latency. cur lists acceptable bid currencies; we explicitly choose INR.

Inside that envelope, imp[] contains opportunities being offered, not records of ads already displayed. Each entry has its own id, format constraints, and floor. The page context sits outside the array because it applies to the request as a whole. These relationships follow the request and impression object definitions.

A single OpenRTB request, auction-8f3a1c, holds auction rules (first price, tmax 120 ms, INR bids) and shared site context (wanderlust.example) that apply to every impression object. Two separate impression objects follow: slot-1 offers a 300 by 250 banner at a 70 rupee CPM floor, and slot-2 offers a 728 by 90 banner at a 50 rupee CPM floor; both require secure creative. Figure 1. One request, two opportunities. Shared context surrounds two independently identified opportunities. A size or floor belongs to its slot; a request ID covers both. Two sizes inside one banner would be alternatives; two imp entries are separate opportunities.

Reading slot-1 in plain language: “A 300 × 250 banner may compete here, with a ₹70 CPM floor, and the creative must use secure delivery.” Slot-2 has a different shape and a different minimum. A DSP with only an approved 300 × 250 creative may bid for slot-1 and leave slot-2 unanswered.

Now change the thought experiment: suppose slot-1’s banner.format listed both 300 × 250 and 300 × 600. That would describe alternative acceptable sizes for one opportunity, not two separate purchases. The Imp boundary defines the opportunity; the sizes inside it are alternatives. Likewise, an opportunity can offer more than one media type; an individual bid must fit an offered type.

The ₹70 floor is an admission constraint; it says nothing about the eventual clearing price. The two entries are also independently awardable impressions; OpenRTB has separate grouping semantics for all-or-nothing purchases in integrations that support them.

What the short payload leaves out

We deliberately omit device information, user identifiers, privacy strings, and supply-chain detail to keep the message readable. A real sender must populate applicable signals and enforce its sharing policy; a real receiver needs a defined response to missing required context. Omission is not evidence of permission.

The following fields answer different questions:

Object or fieldQuestion it helps answer
site or appIn what website or application context will the ad appear?
deviceWhat permitted device characteristics are available?
userWhat permitted audience information or identifiers are available?
regsWhat applicable regulatory and privacy signals accompany the request?
source.schainWhat selling-chain participants are declared?
imp.pmpWhat private-marketplace opportunities and deal terms are offered?

These are separate object families in the specification; each row links to its object definition. A declared supply chain says nothing about audience consent, and a page URL carries no persistent identity. Some forms of contextual bidding still work without any identifier; our identity article develops that last distinction.

For the rest of this example, assume both partners have approved the permitted contextual inputs and Urban Hiker is eligible to buy this inventory. That lets us study the auction mechanics without inventing a plausible-looking but meaningless consent string.

Read identity and provenance in their own namespaces

Suppose a permitted request includes user.id: "exchange-u42" and user.buyeruid: "buyer-u91". Those can refer to the same recognized browser in two different systems. The first belongs to the exchange’s namespace; the second is the buyer-specific identifier mapped for that recipient. Joining the first directly to the DSP’s audience table as if it were the second can miss a valid match or create a false one. The User object defines that distinction. Neither field promises a permanent, universal identity for a human.

Location needs similar care. device.geo describes location associated with the device; user.geo describes the user’s home base and need not describe the current visit. A coarse country signal is different from precise coordinates with a source, accuracy, and age. A buyer should not silently turn “location unknown” into “outside the campaign’s geography,” or assume every location value has GPS precision. The Device and Geo definitions establish what the fields represent; the campaign defines how missing or uncertain inputs affect eligibility.

Privacy signals are also structured inputs. For example, regs.gpp can carry a GPP string and regs.gpp_sid identifies applicable sections; under the relevant framework, user.consent can carry a TCF consent string. We must interpret the applicable framework rather than test whether a string is merely nonempty. These regulatory fields carry the signals; enforcing the resulting restrictions is the job of the systems that read them.

Supply provenance answers another question. In a source.schain node, asi names the advertising system and sid identifies the seller account within that system. The same seller-account string at two systems need not mean the same entity. hp describes participation in payment flow, while complete: 1 declares that the chain reaches back to the inventory owner. The SupplyChain definitions describe a selling and payment path, not an inventory of every company receiving request data.

For our troubleshooting exercise, (exchange.example, seller-42) is therefore a pair to investigate, not proof that Wanderlust Weekly authorized that seller. Completeness, likewise, is a declaration to validate rather than a fraud-free certificate. Keeping identity, privacy, and supply provenance separate prevents one plausible-looking field from being treated as proof of all three. The supply-chain investigation works through the account joins and deliberately breaks them.


3. The Deadline Includes the Journey

The exchange sends the request and starts a timer. tmax: 120 gives the bidder less than 120 milliseconds of model execution, because the allowance includes getting the request to the bidder and getting its response back.

Suppose the outbound trip takes 15 ms. Parsing and campaign eligibility take 10 ms, feature retrieval and scoring take 45 ms, and response construction takes 5 ms. The return trip takes another 20 ms. The bid reaches the exchange after 95 ms, leaving 25 ms before our example’s 120 ms receipt deadline.

Timeline allocating a 120 millisecond bid deadline: 15 milliseconds outbound, 10 eligibility, 45 scoring, 5 response construction, and 20 return, with receipt at 95 milliseconds and 25 milliseconds of headroom. The bidder's own steps are drawn solid and the network legs tinted. A competing 92 rupee CPM bid arrives at 135 milliseconds, beyond the deadline, and is excluded. Figure 2. The network spends the budget too. Only part of the response allowance, which runs from exchange dispatch to response receipt, is available for scoring: the bidder’s work uses 60 ms and the network another 35 ms. The higher bid shown below the main timeline misses the exchange’s receipt deadline, so it cannot compete under this timeout policy.

Suppose a competing DSP offers ₹92 CPM but its response arrives at 135 ms. The JSON can be well formed, the creative approved, and the valuation sensible; the response is still late under our stated policy. Waiting for it would change the latency promise made to the upstream caller.

An engineering consequence of the definition of tmax is that the bidder needs an internal budget smaller than the exchange’s entire allowance. Queueing, network variability, and serialization compete with model work. A more accurate model that regularly misses the response window may contribute less useful demand.

There can also be an earlier publisher or wrapper deadline upstream. Our 120 ms clock covers this exchange-to-bidder interaction; it does not promise that the full page loads in 120 ms or that a returned exchange winner will survive every later selection step.

This gives us a practical debugging split. A deliberate no-bid says the bidder chose not to participate. A timeout says the exchange did not receive a usable response in time. Those outcomes require different investigations, even if both leave the slot without an offer from that DSP.


4. A Response Contains Offers, Not Just a Number

Urban Hiker has an eligible 300 × 250 creative. Its DSP bids for slot-1 and makes no offer for slot-2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  "id": "auction-8f3a1c",
  "bidid": "response-61",
  "cur": "INR",
  "seatbid": [
    {
      "seat": "buyer-7",
      "group": 0,
      "bid": [
        {
          "id": "bid-17",
          "impid": "slot-1",
          "price": 85.0,
          "adomain": ["urbanhiker.example"],
          "crid": "boots-300x250-v3",
          "w": 300,
          "h": 250,
          "adm": "<a href=\"https://urbanhiker.example/boots\"><img src=\"https://cdn.urbanhiker.example/boots-300x250.png\" width=\"300\" height=\"250\" alt=\"Urban Hiker waterproof boots\"></a>",
          "nurl": "https://bidder.example/win?bid=bid-17&auction=${AUCTION_ID}&imp=${AUCTION_IMP_ID}&price=${AUCTION_PRICE}",
          "burl": "https://bidder.example/bill?bid=bid-17&auction=${AUCTION_ID}&imp=${AUCTION_IMP_ID}&price=${AUCTION_PRICE}",
          "lurl": "https://bidder.example/loss?bid=bid-17&auction=${AUCTION_ID}&imp=${AUCTION_IMP_ID}&reason=${AUCTION_LOSS}"
        }
      ]
    }
  ]
}

The top-level id echoes the request. seatbid[] groups offers by buyer seat; a seat identifies the buying entity represented by the bidder in this integration. Inside it, bid[] holds individual offers. The response has one bid even though the request offered two slots. group: 0 explicitly uses independently awardable impressions.

The offer combines a price with a proposed ad. adomain identifies the advertiser domain for checks; crid identifies the creative for tracking and review; adm carries the creative markup. Our tiny HTML string shows transport only; production creative approval, isolation, tracking, and delivery behavior require more than these few tags. The response object definitions describe these fields and their optionality.

The IDs have different jobs

bid.impid joins the offer to imp.id, not to a position in the array. However the response is ordered, bid-17 is trying to buy slot-1.

Request auction-8f3a1c joins to a response with the same ID and bidid response-61. Within the request, slot-1 joins by impid to bid-17, priced at 85 INR CPM under seat buyer-7, while slot-2 has no bid. The creative ID is a separate reusable asset identifier. Figure 3. Follow the IDs, not the array positions. A response can contain fewer bids than the request contains opportunities, so we join by explicit identifiers, not array order. The creative can recur across auctions; this particular offer belongs to one request and one slot.

Imagine logging only auction-8f3a1c and a price. If a later response contained offers for both slots, we could no longer tell which price belonged to which placement. Logging only boots-300x250-v3 would be worse: that creative might participate in thousands of unrelated requests.

For our troubleshooting records, we therefore retain the exchange/partner namespace, request ID, impression ID, seat, and bid ID, plus event type and time. That combination is our application’s logging design; OpenRTB makes no promise that every identifier is globally unique. response-61 tracks the response as a whole; bid-17 tracks the individual offer.

One macro deserves care: ${AUCTION_BID_ID} expands from BidResponse.bidid, despite its tempting name. It would produce response-61 here, not bid-17. Our bidder inserts its own individual bid ID literally into each notice URL. This follows the specification’s macro mapping.


5. Validate First, Then Compare Prices

A useful auction trace records why each offer did or did not reach price comparison. For slot-1, assume the exchange admits only timely, compatible offers that meet the ₹70 floor, then runs a simple first-price comparison with no discounts or special priorities:

CandidateBid, INR CPMArrivalValidation resultOutcome
Urban Hiker8595 msEligible 300 × 250 creativeWins
Buyer B8088 msEligible 300 × 250 creativeLoses on price
Buyer C92135 msMisses the 120 ms deadlineExcluded
Buyer D11070 msCreative is 728 × 90; slot-1 accepts only 300 × 250Excluded
Buyer E6560 msBelow slot-1’s ₹70 floorExcluded

This table explains why the largest number in the logs need not be the winning bid. Paying more cannot fix Buyer D’s wrong shape or Buyer C’s lateness. If either problem were mistakenly classified as “lost on price,” a bidding model might learn to increase bids for a failure that money cannot solve.

We also need aligned units. bid.price is a CPM quote in the response currency, while bidfloor uses bidfloorcur. Our request and response make INR explicit, so comparing 85 with 70 is meaningful. Omitting the response currency would invoke the specification’s USD default, not “inherit INR because the request mentioned it.” Currency defaults and price units are defined in the response specification.

In our stipulated first-price auction, the clearing price is ₹85 CPM. For one subsequently billable impression, the media amount is ₹85 ÷ 1,000 = ₹0.085. For 10,000 such impressions, it is ₹850. The ₹1,000 ledger example adds ₹100 of DSP charges and ₹50 of verification charges to that media subtotal. Those separately assumed charges are not encoded by setting this media bid to 1,000.

The floor, offered price, clearing price, and all-in advertiser cost answer different questions. Our example makes the bid and clearing price equal by assumption. With another auction type, deal terms, or an agreed adjustment, that equality needs checking.

A private deal changes which offer we are making

Now consider a separate variant of slot-1. Wanderlust Weekly offers it through a private marketplace, or PMP, deal with a ₹80 CPM minimum for buyer-7. Replace slot-1’s object in the original request with this object; the rest of that request stays the same:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "id": "slot-1",
  "banner": {"format": [{"w": 300, "h": 250}]},
  "bidfloor": 70.0,
  "bidfloorcur": "INR",
  "secure": 1,
  "pmp": {
    "private_auction": 1,
    "deals": [{
      "id": "outdoor-deal-9",
      "bidfloor": 80.0,
      "bidfloorcur": "INR",
      "at": 1,
      "wseat": ["buyer-7"]
    }]
  }
}

This object restricts the existing slot-1. private_auction: 1 limits bids to the specified deals and their terms. The deal’s wseat names the eligible buyer seat. A larger open-market offer does not bypass that restriction. The Pmp and Deal objects define these controls.

To make its ₹85 CPM offer under this deal, the DSP adds "dealid": "outdoor-deal-9" to bid-17. It keeps impid: "slot-1" because the placement is unchanged. The exchange can now check the slot, deal ID, seat, creative, and ₹80 deal floor together. An offer of ₹75 passes the original ₹70 floor but fails this deal’s price requirement, so the earlier open-auction table no longer decides the winner.

Two details make the variant useful beyond the example. A deal’s at can override the request’s auction type; here both explicitly say first price. A deal’s bidfloorcur does not inherit from Imp.bidfloorcur: omitting it would default to USD. These rules belong to the Deal definition, so our deal makes INR explicit too.

A deal ID names terms that both sides must understand; by itself it promises no fixed price, guaranteed volume, or win. This is how a negotiated buying arrangement becomes part of an individual auction message.


6. The Winning Response Splits into Two Paths

The exchange has Urban Hiker’s markup already, because it arrived in adm. It can pass that markup towards the publisher while notifying the bidder of the exchange win. Fetching the image from the creative CDN happens later on the delivery path.

In three lanes, publisher and browser, exchange, and bidder endpoints, the selected bid-17 at 85 rupees CPM branches into a server win notification to the bidder's nurl and a separate markup delivery path to the page, where the creative loads from the CDN. A valid recorded render becomes counting evidence at the exchange, which then sends a billing notice to burl. A separate losing bid follows an optional lurl loss-notice path. Figure 4. One win, two paths forward. With inline adm, the win notice does not have to return the creative, but delivery still has to happen. This example sends billing notification after an agreed valid render event, which a win notice alone cannot establish; the arrows show dependencies, not guaranteed notification arrival order.

OpenRTB also supports an alternative in which the winning bidder returns markup in the response to nurl. The ad-serving section describes both approaches. Inline markup avoids making its availability depend on that additional response, though later asset requests can still fail.

The three notice URLs in our bid serve distinct purposes:

FieldEvent representedWhat we cannot infer from it alone
nurlThis bid won the exchange auctionThe browser displayed the ad
burlThe winner reached the exchange’s agreed billable conditionThe viewer paid attention or later bought boots
lurlThis bid is known to have lost, where supportedEvery loser receives a clearing price or detailed explanation

For continuity with our ledger article, assume the billable condition is a valid, recorded render event. The browser-side delivery system supplies evidence of that event; the exchange sends the billing notice server to server. The IAB’s counting guidance discusses several approaches and the need for counterparties to agree on the event being counted.

A URL is a template until the exchange fills it

The bidder supplied ${AUCTION_ID}, ${AUCTION_IMP_ID}, and ${AUCTION_PRICE} placeholders. Under our no-discount first-price assumptions, substituting them into the win URL gives:

1
https://bidder.example/win?bid=bid-17&auction=auction-8f3a1c&imp=slot-1&price=85

Here price=85 still means ₹85 CPM, not ₹85 for one impression. The macro carries the clearing price, not necessarily the offered bid. In a separate second-price-plus example where an ₹85 bid clears at ₹80.01, the substituted value would be 80.01. The actual pricing and increment rules belong to the exchange, and a production integration may use an agreed price encoding instead of this readable value. URL escaping and the handling of missing macro values also belong in the integration checks.

Now suppose Priya closes the page before the creative renders. The exchange win may already have been reported, but our stipulated billable condition never occurs. Or suppose the render occurs and the notification transport fails. A missing callback then describes an evidence gap, not proof that no render happened. Reconciliation needs the underlying event records.

Delivery delay has its own guidance: imp.exp advises how many seconds may elapse between auction and impression, and bid.exp advises how long the bidder is willing to wait. These are different from the millisecond tmax response deadline. The expiration guidance explicitly cautions against treating the request’s advisory value as a contract unless the exchange says otherwise. Exceeding it does not automatically cancel a charge; that consequence requires an agreed policy.

The same caution applies to losing bids. Exchanges can restrict loss notifications and price disclosure. Missing feedback must remain missing; turning an absent price into zero would invent a training label. Our win-curve article explains why incomplete auction outcomes matter for learning market prices.

One billable event can produce more than one callback

Suppose the billing endpoint records bid-17, but its HTTP acknowledgment is lost. If the sender retries, a second callback can describe the same billable event. Counting HTTP requests as purchases would book two impressions from one.

For this single-impression example, the receiver can use the partner namespace, request, slot, bid, and event type as its agreed logical key. The first validated billing notice records ₹0.085; an identical retry leaves that amount unchanged. A win notice is a different event type and must not create a second charge. This is idempotent processing: replaying the same event does not repeat its financial effect.

That key is an application design, not a universal OpenRTB event-ID guarantee. Integrations with multiple legitimate events or adjustments need suitable event identifiers and adjustment rules. A conflicting price on an apparent duplicate should become an exception to investigate, not silently overwrite the first amount. The receiver should also validate the sender under the integration’s agreed authentication mechanism; an auction ID alone does not authenticate a callback.

This links protocol implementation to the campaign’s spending controls: missing notices can leave reservations unresolved, while duplicate notices can overstate spend. Reliable bidding depends on interpreting the messages correctly after the auction as well as before it.


7. Keep “No Bid,” “Lost,” and “Not Billed” Separate

We can now diagnose the transaction with a sequence of narrower questions. Did the request arrive and parse? Did the buyer find an eligible campaign? Did an offer arrive before the deadline? Did it pass validation? Did it win this auction? Did the creative reach the agreed counting point?

Those questions lead to different records:

Observed outcomeInterpretationFirst place to investigate
HTTP 204, empty bodyIntentional no-bidEligibility, budget, targeting, internal decision logs
No usable response before deadlineTimeoutNetwork, queueing, scoring latency
Bid rejectedAn offer failed an admission checkIDs, dimensions, currency, policy, floor
Eligible bid lostAdmitted offer was not selectedAuction rules and competitive price
Exchange win, no billable-event recordSelection occurred; billing evidence is absentUpstream selection, delivery, counting, transport

If the bidder wants to return a reason for declining the whole request, OpenRTB also supports a no-bid response using nbr. The no-bid guidance describes the supported response forms. Leaving slot-2 out of our otherwise valid bid response simply means this buyer offered nothing for that slot; the slot-1 offer stands.

For an operational dashboard, these distinctions determine the denominator. A timeout rate concerns requests awaiting responses. An auction win rate concerns the defined set of participating bids. A render rate concerns selected ads progressing through delivery. Combining all failures into one “lost impression” bucket hides where the system needs attention.

We now have a concrete route through the protocol: a request names opportunities, a response attaches offers to those opportunities, the exchange evaluates eligible offers within its deadline, and later events establish what was delivered and billable. In the next article, we build the campaign behind that response: how the advertiser’s brief becomes line items, targeting, creative approvals, budgets, and spending controls. Those rules define the job that the bidder’s prediction and optimization models must perform.


Resources

  • IAB Tech Lab, OpenRTB 2.6 specification: transport, request and response objects, creative delivery, and substitution macros. The examples and explanations above are original adaptations of the interface, which is published under a Creative Commons Attribution license.
  • IAB Tech Lab, OpenRTB implementation guidance: no-bid signaling, impression expiration, and counting billable events.
Enjoyed this article? Never miss out on future posts - follow me.
© Sayan Biswas. All rights reserved.