Answers

How do I build an OpenRTB bidder endpoint?

Short answer

A bidder is one HTTPS endpoint that accepts a POSTed JSON bid request and returns a bid response before tmax expires. The minimum viable implementation parses the request, decides on each imp, and returns either HTTP 200 with a BidResponse whose id echoes the request id and whose bids carry impid, price, mtype, and adm or nurl, or HTTP 204 with no body to pass. Everything else, targeting, pacing, shading, sits behind that contract. The hard parts are latency (your compute budget is tmax minus network round trip) and honouring what the request declares: cur, wseat, bcat, badv, and privacy signals.

The contract, in one paragraph

An OpenRTB bidder is an HTTPS endpoint. The exchange POSTs a JSON bid request to it and waits up to tmax milliseconds. You return either a bid response or nothing. There is no session, no handshake, and no retry: each request is independent, and an answer that arrives late is the same as no answer at all. Everything a DSP is, targeting, pacing, budgeting, creative selection, pricing, lives behind that single request-response.

The minimum viable bid response

{
  "id": "80ce30c53c16e6ede735f123ef6e32361bfc7b22",
  "cur": "USD",
  "seatbid": [{
    "seat": "512",
    "bid": [{
      "id": "1",
      "impid": "1",
      "price": 3.75,
      "adid": "314",
      "adomain": ["advertiser.com"],
      "crid": "creative-112",
      "mtype": 1,
      "adm": "<div>…creative markup…</div>",
      "w": 300,
      "h": 250
    }]
  }]
}

Field by field: id must echo BidRequest.id or the exchange discards the response (loss reason 5, Invalid Auction ID). impid must match an imp.id from the request. price is CPM in the response currency, which must be one the request allowed in cur. mtype declares the markup type (1 banner, 2 video, 3 audio, 4 native) and exists so the exchange does not have to guess from the markup. adomain and crid are optional in the spec and mandatory in practice: exchanges filter bids without them (loss reasons 6 and 8).

Either put the markup inline in adm or serve it from the win notice nurl. The tradeoffs, including where ${AUCTION_PRICE} expands, are in adm versus nurl.

What the request obliges you to honour

  • cur lists the currencies the exchange accepts. Bidding in anything else is invalid, and RTBlint flags it as openrtb.response.cur_not_allowed when it cross-validates a response against its request.
  • wseat and bseat restrict which seats may bid. Bidding from a seat that was not permitted gets the bid dropped (loss reason 104).
  • bcat, badv, bapp are blocklists for creative categories, advertiser domains, and promoted apps. Filtering these on your side is cheaper than being filtered on theirs.
  • imp.bidfloor and bidfloorcur set the minimum. Deal-backed bids clear the deal floor in imp.pmp.deals[].bidfloor, not the impression floor.
  • Privacy signals. regs.gdpr, user.consent, regs.gpp and gpp_sid, regs.coppa, and device.lmt govern what you may do with the data in the request. See the privacy signals reference.
  • Media type. Bid on what was offered: video markup against an imp that only carried banner is rejected (loss reason 204), and RTBlint reports openrtb.bid.mtype_not_offered.

Latency is the hard requirement

The spec is explicit that tmax includes internet latency, so your compute budget is what remains after the wire. Three consequences worth designing for from day one: deploy per region so the round trip is short, enforce an internal deadline that returns a no-bid instead of a late bid, and measure the tail rather than the mean, because timeouts live at p99. The debugging sequence is in tmax and bidder timeouts.

Two transport settings pay for themselves immediately: connection reuse (keep-alive or HTTP/2), which removes a TLS handshake from every auction, and gzip, which cuts the bytes on a payload that is mostly repetitive JSON.

An integration checklist

  1. Parse defensively. Unknown fields must be ignored, not rejected: exchanges add extensions and newer snapshots constantly. See what happens on a version mismatch.
  2. Decide the version you accept and what you do with x-openrtb-version, then check real traffic matches it.
  3. Return 204 for no bid; return 200 with nbr while integrating so the partner can see your reasons. Codes are in no-bid reason codes.
  4. Implement burl handling if you bill on the billing notice rather than the win notice, and expect lurl feedback for pricing.
  5. Validate both directions in CI against fixtures, including a paired request and response so id, seat, currency, deal, and media type are checked together.
  6. Load test at the QPS the exchange expects, with the latency budget enforced.

Testing it

Start from a known-good payload: the example payloads cover banner, video, CTV pod, native, DOOH, and privacy-heavy requests, and each opens directly in the tester. In CI, the CLI validates requests and responses with --format json so you can gate on specific rule ids, and the JSON Schemas plug into whatever validation layer your service already has. The full CI pattern is in OpenRTB validation in CI.

Sources