How ad-tech impression tracking works (nurl, burl, ext.imptrackers)

An impression sounds like a simple thing: the ad was shown, fire a pixel. In practice there are four different impression-counting mechanisms in OpenRTB, each fires at a different moment, and DSPs and SSPs perpetually disagree about which one is the truth.

Why impression tracking is complicated

If you read the OpenRTB spec from end to end, you find that there are four places a DSP can put a URL that will fire on impression, and each one fires under different conditions. This was not designed up front; it accumulated over a decade of trying to solve specific problems — auction reconciliation, billing reconciliation, post-render verification, multi-step wrapper accounting. The four mechanisms are:

Understanding when each one fires — and which mechanism the SSP and DSP each use as their source of truth — is the foundation of all reconciliation work in programmatic.

nurl — the win notice

The bid.nurl field contains a URL that the SSP fires when the DSP wins the auction. Notably, this is before any user has actually seen the ad — it fires at auction close, which is server-side, on the SSP's machines, microseconds after the auction wraps up.

The SSP performs the macro substitution (we cover macros below) and then makes an HTTP GET against the URL. From the DSP's perspective, this is the first signal that "we won this auction." DSPs use nurl for:

What nurl does not mean: it does not mean the ad rendered. The user might have closed the tab between the auction closing and the creative loading. The SSP might decide post-hoc that the auction was fraudulent and not actually deliver the creative. The creative might fail to load. In all of those cases nurl fires anyway. This is exactly why burl exists.

Common pitfall: early DSPs counted nurl as the canonical impression and billed advertisers based on it. SSPs then started filtering invalid impressions — discarding ones that didn't actually render — and the DSP would still bill the advertiser for them. The result was tens of millions of dollars in mismatched invoicing per year, per major DSP. burl was added in OpenRTB 2.5 to fix this.

burl — the billing notice

The bid.burl field contains a URL that the SSP fires when the impression is billable. The exact definition of "billable" is up to the SSP, but in practice it means "the creative was actually delivered to the page and met whatever rendering criteria the SSP applies." Some SSPs apply MRC-style criteria (50% of pixels in view for 1 second for display, 50% for 2 seconds for video). Others fire burl as soon as the creative HTML body is delivered to the browser and any embedded tracker would have fired.

From the DSP's perspective, burl is the closest thing to a canonical billable-impression signal that exists in the protocol. Most modern DSPs count burl as the source of truth for advertiser billing, and use nurl only for internal pacing.

A typical pattern in the bid response:

{
  "nurl": "https://dsp.example.com/win?auc=${AUCTION_ID}&bid=${AUCTION_BID_ID}&price=${AUCTION_PRICE}",
  "burl": "https://dsp.example.com/bill?auc=${AUCTION_ID}&bid=${AUCTION_BID_ID}&price=${AUCTION_PRICE}",
  "lurl": "https://dsp.example.com/loss?auc=${AUCTION_ID}&reason=${AUCTION_LOSS}"
}

(lurl is the loss notice — fired when the bid lost. It's symmetric with nurl and useful for the DSP to understand why it lost. ${AUCTION_LOSS} resolves to one of the loss-reason codes defined in OpenRTB 5.24 — values like 102 "bid blocked", 351 "bid below floor", 1003 "lost to higher bid in same auction".)

ADM-embedded pixels

The bid.adm field is the actual creative — the HTML, the JavaScript, or in video the VAST XML. Most DSPs put an additional impression pixel directly inside the ADM:

<img src="https://dsp.example.com/imp?auc=ABC&price=${AUCTION_PRICE}"
     width="1" height="1" style="display:none" />
<a href="${CLICKTAG}https://acme-advertiser.com/spring-sale">
  <img src="https://cdn.example.com/banner-300x250.jpg" width="300" height="250" />
</a>

This pixel fires in the user's browser, not on the SSP's server. It fires only if the creative actually renders far enough for the image tag to be parsed and the HTTP request to leave the browser. Crucially, it fires after the browser actually loaded and started painting the ad — so it is the closest to a "user actually saw the ad" signal that exists, short of full viewability measurement.

Many DSPs run a three-way reconciliation: nurl (auction won), burl (SSP-confirmed render), and ADM-pixel (DSP-confirmed render). The triplet usually agrees to within a few percent. When it doesn't, that's a signal worth investigating — either the SSP is shipping burl too aggressively, or the creative is failing to load.

ext.imptrackers and the wrapper case

The fourth mechanism is bid.ext.imptrackers, an array of URLs the SSP or wrapper is asked to inject into the rendered page. This is mostly used when the DSP's adm is going to be wrapped (the SSP will rewrap it inside a top-level container) and the DSP wants extra pixels added.

For video, the equivalent is the <Impression> element inside a VAST wrapper — discussed in understanding VAST tags. The wrapping SSP appends its own <Impression> URLs to the chain so that the final player fires every level's pixel when the ad plays.

ADM method vs URL method

The OpenRTB spec defines two ways the SSP delivers the creative to the publisher's page:

ADM method

The DSP returns the creative HTML in bid.adm. The SSP forwards that HTML directly to the page. nurl still fires server-side on win. This is the modern default — fewer round trips, lower latency, less surface area for tampering.

URL method (legacy)

The DSP leaves bid.adm empty and puts a URL in nurl. The SSP fires nurl as usual, but with the expectation that the response body will be the creative HTML, which the SSP then serves to the page. This double-purposes nurl as both win notice and creative delivery. It was common in OpenRTB 2.2 and earlier; most exchanges no longer support it.

If you are integrating a new DSP today, use the ADM method exclusively. Some exchanges (notably some CTV-focused ones) require the ADM method by contract.

Macro substitution

The URLs in nurl, burl, lurl and any tracker inside adm all support a set of substitution macros. The SSP fills these in before firing the URL (or before delivering the ADM to the page, in the case of ADM-embedded macros). The standard ones from OpenRTB 4.4:

MacroSubstituted with
${AUCTION_ID}BidRequest.id
${AUCTION_BID_ID}BidResponse.bidid
${AUCTION_IMP_ID}Imp.id
${AUCTION_SEAT_ID}SeatBid.seat
${AUCTION_AD_ID}Bid.adid
${AUCTION_PRICE}The clearing price the SSP confirms to the DSP, in plaintext
${AUCTION_PRICE:B64}The clearing price, base64-encoded
${AUCTION_CURRENCY}The currency, e.g. USD
${AUCTION_LOSS}Loss reason code (in lurl only)
${AUCTION_MBR}The minimum bid to win (post-auction transparency, optional)

${AUCTION_PRICE} has a security wrinkle worth knowing. If the price is sent in plaintext in the URL, a sophisticated user could in principle decode the URL and learn what the publisher was paid for the ad. To prevent this, the spec offered an encryption scheme (mostly forgotten now) and the :B64 variant for base64 — neither is true protection, but they obscure casual inspection. The most robust approach is to render the URL server-side and never pass ${AUCTION_PRICE} through the client at all.

Tip: when an impression URL is failing because a macro didn't substitute, you will see the literal string ${AUCTION_PRICE} in your logs. If you see that, it means either the SSP doesn't support that macro, or the URL was put somewhere the SSP doesn't process (some SSPs do not substitute macros inside adm, only in nurl/burl). The URL decoder tool is handy for spotting these literally.

Why your impression counts will never match

If you compare the impression count from any SSP and any DSP for the same campaign over the same day, they will disagree. This is not because either side is lying — it is structural. A non-exhaustive list of causes:

  1. Different counting events. The SSP may count nurl firing; the DSP may count burl firing. Those are different events that happen at different times.
  2. Network drops. The browser pixel inside adm can fail to fire — slow page exit, blocked tracking domain, network error. The SSP counted the impression (it served the creative); the DSP did not (its pixel never fired).
  3. Adblockers. Affect ADM pixels more than they affect burl, because burl is server-to-server.
  4. SSP fraud filtering. Most SSPs run post-auction fraud filtering and decline to fire burl for sessions classified as invalid. The DSP fired nurl already; it counts the auction win. The SSP does not bill it.
  5. Time zone alignment. A 24-hour window in the SSP's tz vs the DSP's tz catches different sets of impressions at the boundaries.
  6. Caching. Some SSPs cache the creative briefly and reuse it within the cache window — the DSP wins one auction but the creative renders twice. Conversely, some DSPs deduplicate by user-creative-impression.

A discrepancy of 1 to 3 percent is normal and expected. A discrepancy of 10 percent or more is a signal to investigate. The first step is always: confirm which event each side is counting, in plain English, before looking at logs.

Viewability — a different question

Worth distinguishing: "the impression fired" is not the same as "the user actually saw the ad." Viewability is measured separately, usually by third-party vendors (IAS, DV, Moat) injected via the IAB's Open Measurement Interface Definition (OMID). The MRC standards are:

An "impression" in OpenRTB tracking terms can fire on an ad that does not meet these criteria — e.g. an ad rendered far below the fold that the user never scrolled to. This is one reason advertisers increasingly buy on viewable-impression CPMs (vCPM) rather than served-impression CPMs.

Practical tips

Things we have learned the hard way and now do reflexively:

Closing thought

Impression tracking is one of the parts of programmatic advertising where the engineering and the accounting meet, and it is the engineering that has to be airtight for the accounting to come out right. The good news is that the mechanisms are well-specified; the bad news is that there are four of them, and every integration has subtle quirks about which one is the source of truth. Knowing what each one means and when it fires turns most reconciliation arguments into a five-minute conversation rather than a multi-day audit.


Related reading