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
curlists the currencies the exchange accepts. Bidding in anything else is invalid, and RTBlint flags it asopenrtb.response.cur_not_allowedwhen it cross-validates a response against its request.wseatandbseatrestrict which seats may bid. Bidding from a seat that was not permitted gets the bid dropped (loss reason 104).bcat,badv,bappare blocklists for creative categories, advertiser domains, and promoted apps. Filtering these on your side is cheaper than being filtered on theirs.imp.bidfloorandbidfloorcurset the minimum. Deal-backed bids clear the deal floor inimp.pmp.deals[].bidfloor, not the impression floor.- Privacy signals.
regs.gdpr,user.consent,regs.gppandgpp_sid,regs.coppa, anddevice.lmtgovern 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
impthat only carriedbanneris rejected (loss reason 204), and RTBlint reportsopenrtb.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
- Parse defensively. Unknown fields must be ignored, not rejected: exchanges add extensions and newer snapshots constantly. See what happens on a version mismatch.
- Decide the version you accept and what you do with
x-openrtb-version, then check real traffic matches it. - Return 204 for no bid; return 200 with
nbrwhile integrating so the partner can see your reasons. Codes are in no-bid reason codes. - Implement
burlhandling if you bill on the billing notice rather than the win notice, and expectlurlfeedback for pricing. - Validate both directions in CI against fixtures, including a paired request and response so id, seat, currency, deal, and media type are checked together.
- 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.