OpenRTB for publisher and SSP engineers

On the sell side you own the bid request: what it says about the impression, how accurate that is, and how quickly it leaves. You also run the auction and fire the notices that bidders count spend against. This page lists those duties, the fields buyers price on, the failures that cost revenue, and how to test before a partner goes live.

What a sell-side engineer owns

  • The request contract. One complete BidRequest per auction, valid for the version you declare in x-openrtb-version, with exactly one of site, app, or dooh.
  • Inventory identity. Domains, bundles, store URLs, and publisher IDs that agree with ads.txt, app-ads.txt, and your sellers.json.
  • Consent and identity. Privacy strings in their 2.6 paths, and user identifiers passed only where those strings allow.
  • Supply chain. A source.schain that lists every hop, with your own node last.
  • Price rules. Floors, floor currencies, deals, and the auction type.
  • Time. A tmax that reflects what is left of your own budget, and a fan-out that calls bidders in parallel.
  • The auction. Checking bids against the request, applying blocks, and setting the clearing price.
  • Notices and documentation. Firing nurl, burl, and lurl with macros expanded, and publishing what each event means and which ext fields you send.

A request that answers the buyer's questions

Before pricing, a buyer wants to know what the impression is, where it runs, who sees it, which companies sit between it and the publisher, and what it costs. This in-app video interstitial answers each of those:

{
  "id": "6c1f0b9e-3a27-4d55-8e0c-b4a2d97f1e63",
  "at": 1,
  "tmax": 250,
  "cur": ["EUR", "USD"],
  "bcat": ["IAB25", "IAB26"],
  "imp": [
    {
      "id": "1",
      "tagid": "level-end-interstitial",
      "instl": 1,
      "secure": 1,
      "exp": 1800,
      "bidfloor": 3.00,
      "bidfloorcur": "EUR",
      "video": {
        "mimes": ["video/mp4"],
        "plcmt": 3,
        "minduration": 5,
        "maxduration": 30,
        "protocols": [7, 8],
        "w": 1080,
        "h": 1920,
        "skip": 1,
        "skipafter": 5
      },
      "pmp": {
        "private_auction": 0,
        "deals": [
          { "id": "dl-games-eu-q4", "bidfloor": 6.50, "bidfloorcur": "EUR", "at": 1, "wseat": ["agency-311"] }
        ]
      }
    }
  ],
  "app": {
    "id": "app-5531",
    "bundle": "com.example.puzzlequest",
    "storeurl": "https://play.google.com/store/apps/details?id=com.example.puzzlequest",
    "publisher": { "id": "pub-2290" }
  },
  "device": {
    "ua": "Mozilla/5.0 (Linux; Android 14; SM-S921B Build/UP1A.231005.007; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/130.0.0.0 Mobile Safari/537.36",
    "ip": "203.0.113.0",
    "devicetype": 4,
    "os": "Android",
    "osv": "14",
    "ifa": "3f6e1c2a-9b84-4d07-a5e2-6c1d8f0b7a93",
    "lmt": 0,
    "geo": { "country": "DEU", "type": 2 }
  },
  "user": { "consent": "CPXxRfAPXxRfAAfKABENB" },
  "regs": { "coppa": 0, "gdpr": 1, "gpp": "DBABMA~CPXxRfAPXxRfAAfKABENB", "gpp_sid": [2] },
  "source": {
    "tid": "9d0a4f7e-61c2-4b3e-8f5d-2e7c90a1b6d4",
    "schain": {
      "ver": "1.0",
      "complete": 1,
      "nodes": [
        { "asi": "mediation.example", "sid": "pub-2290", "hp": 1 },
        { "asi": "ssp.example", "sid": "acct-88140", "hp": 1, "rid": "6c1f0b9e-3a27-4d55-8e0c-b4a2d97f1e63" }
      ]
    }
  }
}

The media object says what: a video interstitial with imp.video.plcmt 3 (Interstitial), 5 to 30 seconds, and VAST 4.0 inline or wrapper. app.bundle and app.storeurl say where. The device, consent, and identifier fields say who, and the two-node chain shows a mediation platform selling through this SSP. The open floor is 3.00 EUR and the deal floor is 6.50 EUR.

Two details are easy to miss. imp.exp of 1800 warns bidders that the ad may play up to 30 minutes after the auction, which is honest for an ad shown at the end of a game level. And the deal repeats bidfloorcur because Deal.bidfloorcur does not inherit from the impression; left out, it would default to USD.

The fields buyers price on

FieldsWhat buyers use them forTypical sell-side failure
imp.video.plcmt, imp.banner.formatFormat and placement targetingThe old placement integer copied into plcmt; one fixed size sent when several fit
site.page, app.bundleBrand safety, app and domain targeting, ads.txt checksBundle missing, or set to an internal ID
publisher.id, schain sidMatching the seller in sellers.jsonAn internal alias instead of the account ID sellers.json lists
device.devicetype, device.ip, device.suaDevice targeting and invalid traffic checksCTV sent as a personal computer; the server's IP instead of the address closest to the device
device.ifa, user.buyeruid, user.eidsFrequency capping and audience matchingIdentifiers sent while consent says no
regs.gdpr, user.consent, regs.gpp, regs.gpp_sidWhether the buyer may use the data at allPre-2.6 ext paths; gpp without gpp_sid
source.tid, source.schainDeduplication and supply path checkscomplete set to 1 while an upstream hop is missing
imp.bidfloor, imp.bidfloorcur, imp.pmp.dealsPricing, shading, and deal routingDeal floors read in the wrong currency
at, tmax, curThe rules of the auctionat left at its default of 2 in a first-price auction

Consent signals and the supply chain

Put privacy signals where 2.6 defines them: regs.gdpr, user.consent, and the regs.gpp and regs.gpp_sid pair. Buyers that read only the 2.6 paths see no signal at all when you leave them under ext. Then gate identifiers on those signals before fan-out. When GDPR applies, pass IDs only for vendors with consent. When a GPP section signals an opt-out, or regs.coppa is 1, empty the eids array. A structurally valid request can still break the law, so test the gating logic as well as the schema. The privacy reference maps every signal.

For the chain, append your own node with your sellers.json domain as asi and the account you pay as sid. Set complete to 1 only when the first node is the inventory owner's own system. Buyers walk every node against sellers.json and the publisher's ads.txt or app-ads.txt, and they answer failed checks with nbr 11 or 12 for ads.txt and 16 or 17 for the chain. The supply chain reference has the node rules.

Pricing rules

Send at explicitly. Its default is 2, second price plus, so a first-price seller that omits it tells every bidder to bid the wrong way. List the currencies you accept in cur, and keep in mind that bidfloorcur is separate: you may floor in EUR and accept USD, in which case bidders convert before comparing. Deal floors carry their own currency, and a deal with at 3 turns its floor into a fixed price. Duration floors for pods live in imp.video.durfloors. The floors reference and the deals guide cover the details.

The time budget

Your auction usually runs inside someone else's timeout: a header bidding wrapper, a mobile mediation SDK, or an upstream exchange. Compute tmax per request as the budget you have left minus the work you still do after bidders answer (running the auction and replying to your caller). Never raise a tmax you received, and always send milliseconds. A value like 300000 is a unit bug that bidders either clamp or reject.

Close each auction on time even when bidders are still pending, and call bidders in parallel over persistent connections with gzip. If bidders keep answering with nbr 15 (Insufficient Auction Time), your budget is too thin for the routes you send it over. The auction lifecycle page shows where the milliseconds go.

Running the auction and firing notices

Treat every bid response as untrusted input and check it against the request that produced it. BidResponse.id must echo your request ID. Each impid must name an impression you offered, the currency must be in your cur list, the seat must be allowed by wseat or the deal's wseat, a dealid must name a deal you sent, and mtype must be a format the impression accepts. Drop bids that fail, and send the matching loss reason (codes 1 to 10, 104, or the 200 range) when the bidder asked for loss notices.

After the auction, expand macros and fire the notices. nurl goes to the winner once it is chosen. burl should fire server-side at the billable event, close to where you book revenue. lurl carries ${AUCTION_LOSS} and, where your policy allows, ${AUCTION_MIN_TO_WIN}. Replace unknown optional values with an empty string, and use AUDIT when rendering for review. Then write down what triggers each notice. The spec asks exchanges to publish their event triggers and billing policy, and most buyer-side discrepancies trace back to a missing answer here. The macro reference has the full table.

Failure modes that cost revenue

  • Silent rejection. A bidder that cannot parse your request returns 204, or nbr 2 if you are lucky. You see a low bid rate, not an error. Track nbr 2 per bidder and treat a rise as a release regression.
  • Enrichment drift. Fields added late in the pipeline (consent, geo, identifiers) disappear when one service changes. Validate requests after the last enrichment step, not the template.
  • Stale paths. Old SDKs keep sending regs.ext.gdpr, source.ext.schain, or video.placement. Normalize them before fan-out.
  • Wrong defaults. An absent at, bidfloorcur, or secure means something specific. Make every value you intend explicit.
  • Chain overclaims. complete set to 1 with an unknown first hop gets the whole path discounted by buyers that check it.

The ten most frequent request bugs are in common OpenRTB mistakes.

How to test

  1. Capture fixtures after enrichment, one per inventory type you sell (web, app, CTV, DOOH) and per consent state (GDPR applies, GPP present, COPPA set). Validate them in CI against the release you declare; the CI guide shows the setup.
  2. Run live checks with partners using test set to 1, which makes the auction non-billable on both sides.
  3. Ask each bidder to return nbr codes during integration, and review the reasons before switching to production traffic.
  4. Validate responses against the request you sent, using the same checks your auction applies, so a bidder bug shows up as a test failure and not as lost revenue.
  5. Where a buyer publishes its own requirements, check your requests against them too; the exchange notes collect documented dialects.

To see what a complete consent-bearing request looks like in a validator, open the mobile banner with consent sample in the tester and replace it with one of your captured requests.

Questions people ask

Which fields should an SSP always send in an OpenRTB bid request?

Beyond the required id, imp, imp.id, and a media object, send exactly one of site, app, or dooh with its domain or bundle and a publisher ID; a device object with ua, ip, and devicetype; consent signals in their 2.6 paths under regs and user; source.tid and source.schain; and explicit at and tmax values. The spec marks most of these recommended or optional, but buyers rarely bid well without them.

Should an SSP reduce tmax before sending a bid request to bidders?

Yes. tmax is measured at the exchange and includes network latency, so a seller that received a time budget from upstream should forward a smaller value after subtracting its own processing and the extra network hop. It should never increase the value it received, and it must always send milliseconds.

Does a deal floor inherit the impression's floor currency?

No. Deal.bidfloorcur does not inherit from imp.bidfloorcur. It defaults to USD on its own, so a deal priced in euros must set bidfloorcur to EUR on the deal object itself, or bidders will read the deal floor as dollars.