Skip to main content

Tracking & Events

Complete guide to implementing tracking with AdCP creative manifests and OpenRTB standard tracking.

Overview

AdCP supports two complementary tracking mechanisms that work together to provide comprehensive event tracking:

1. OpenRTB Standard Tracking: Bid-level events (win, impression) 2. AdCP Asset-Based Tracking: Creative-level events (impression, click, viewability)

Best Practice: Use both together - OpenRTB tracking for billing/win confirmation, and AdCP asset tracking for detailed creative performance analytics.

OpenRTB Standard Tracking

Use standard OpenRTB burl and nurl fields for bid-level events.

Win Notification (burl)

Fired when the bid wins the auction:

{
"bid": {
"id": "bid-001",
"price": 2.5,
"burl": "https://bidder.com/win?id=bid-001&price=${AUCTION_PRICE}"
}
}

Macro Replacement:

  • ${AUCTION_PRICE} - Replaced by publisher with winning price

When Fired:

  • Immediately after auction completes
  • Before ad is rendered
  • Used for billing and win confirmation

Impression Notification (nurl)

Fired when the ad is actually displayed:

{
"bid": {
"id": "bid-001",
"price": 2.5,
"nurl": "https://bidder.com/imp?id=bid-001"
}
}

When Fired:

  • When ad begins rendering
  • After creative assets load
  • Confirms ad was actually shown

Example with Both

{
"bid": {
"id": "bid-001",
"impid": "imp-1",
"price": 2.5,
"burl": "https://bidder.com/win?id=bid-001&price=${AUCTION_PRICE}",
"nurl": "https://bidder.com/imp?id=bid-001",
"adm": "<a href='...'><img src='...'/></a>"
}
}

AdCP Asset-Based Tracking

Use tracking URLs within creative manifests for detailed creative analytics.

Impression Tracking

Track when the creative is displayed:

{
"assets": {
"impression_tracker": {
"url": "https://track.brand.com/imp?buy={MEDIA_BUY_ID}&cb={CACHEBUSTER}"
}
}
}

When Fired:

  • When creative begins rendering
  • After format validation passes
  • Before user interaction

Click Tracking

Track when user clicks on the creative:

{
"assets": {
"click_tracker": {
"url": "https://track.brand.com/click?buy={MEDIA_BUY_ID}&cb={CACHEBUSTER}"
}
}
}

When Fired:

  • When user clicks on clickable elements
  • Before navigation to landing page
  • Can be fired multiple times per impression

Viewability Tracking

Track when creative is viewable:

{
"assets": {
"viewability_tracker": {
"url": "https://track.brand.com/view?buy={MEDIA_BUY_ID}&duration={VIEW_DURATION}&cb={CACHEBUSTER}"
}
}
}

When Fired:

  • When creative meets viewability criteria (e.g., 50% visible for 1 second)
  • Once per impression
  • Includes view duration if available

Complete Tracking Example

{
"assets": {
"impression_tracker": {
"url": "https://track.brand.com/imp?buy={MEDIA_BUY_ID}&creative={CREATIVE_ID}&device={DEVICE_TYPE}&cb={CACHEBUSTER}"
},
"click_tracker": {
"url": "https://track.brand.com/click?buy={MEDIA_BUY_ID}&creative={CREATIVE_ID}&cb={CACHEBUSTER}"
},
"viewability_tracker": {
"url": "https://track.brand.com/view?buy={MEDIA_BUY_ID}&creative={CREATIVE_ID}&cb={CACHEBUSTER}"
}
}
}

Tracking Flow

Standard Display Ad Flow

AI-Native Contextual Weaving Flow

Publisher Implementation

1. Fire OpenRTB Tracking

Fire burl and nurl at appropriate times:

async function handleWinningBid(bid, auctionPrice) {
// Fire win notification (burl)
if (bid.burl) {
const winUrl = bid.burl.replace('${AUCTION_PRICE}', auctionPrice)
await fetch(winUrl, { method: 'GET', mode: 'no-cors' })
}

// Render creative
await renderCreative(bid)

// Fire impression notification (nurl)
if (bid.nurl) {
await fetch(bid.nurl, { method: 'GET', mode: 'no-cors' })
}
}

2. Fire AdCP Asset Tracking

Fire tracking URLs from creative manifest:

async function fireAdCPTracking(manifest, eventType) {
const trackerKey = `${eventType}_tracker`
const tracker = manifest.assets[trackerKey]

if (!tracker) return

// Replace macros
const url = replaceMacros(tracker.url, {
mediaBuyId: manifest.media_buy_id,
creativeId: manifest.creative_id,
deviceType: getDeviceType(),
country: getCountry(),
domain: window.location.hostname,
cachebuster: Date.now(),
})

// Fire tracking pixel
await fetch(url, { method: 'GET', mode: 'no-cors' })
}

// Usage
await fireAdCPTracking(manifest, 'impression')
await fireAdCPTracking(manifest, 'click')
await fireAdCPTracking(manifest, 'viewability')

3. Handle Click Tracking

Intercept clicks to fire tracking before navigation:

function setupClickTracking(element, manifest) {
element.addEventListener('click', async e => {
e.preventDefault()

// Fire click tracker
await fireAdCPTracking(manifest, 'click')

// Navigate to landing page
const clickthroughUrl = manifest.assets.clickthrough_url.url
const finalUrl = replaceMacros(clickthroughUrl, context)
window.open(finalUrl, '_blank')
})
}

4. Implement Viewability Tracking

Track when creative becomes viewable:

function setupViewabilityTracking(element, manifest) {
const observer = new IntersectionObserver(
entries => {
entries.forEach(async entry => {
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
// Creative is 50%+ visible
setTimeout(async () => {
// Still visible after 1 second
if (entry.isIntersecting) {
await fireAdCPTracking(manifest, 'viewability')
observer.disconnect()
}
}, 1000)
}
})
},
{ threshold: 0.5 }
)

observer.observe(element)
}

Macro Replacement

Replace universal macros before firing tracking URLs:

function replaceMacros(url, context) {
return url
.replace('{MEDIA_BUY_ID}', context.mediaBuyId)
.replace('{CREATIVE_ID}', context.creativeId)
.replace('{CACHEBUSTER}', context.cachebuster || Date.now())
.replace('{DEVICE_TYPE}', context.deviceType)
.replace('{OS}', context.os)
.replace('{COUNTRY}', context.country)
.replace('{DOMAIN}', context.domain)
.replace('{PAGE_URL}', encodeURIComponent(context.pageUrl))
.replace('{TIMESTAMP}', Math.floor(Date.now() / 1000))
.replace('{TIMESTAMP_MS}', Date.now())
}

Privacy Considerations

1. Respect Privacy Signals

Check privacy flags before including personal data:

function buildTrackingContext(manifest) {
const context = {
mediaBuyId: manifest.media_buy_id,
creativeId: manifest.creative_id,
cachebuster: Date.now(),
}

// Only include device/location if tracking allowed
if (!isLimitAdTrackingEnabled()) {
context.deviceType = getDeviceType()
context.country = getCountry()
context.os = getOS()
}

return context
}

Include GDPR/CCPA consent in tracking URLs:

function addConsentMacros(url, consent) {
return url
.replace('{GDPR}', consent.gdpr ? '1' : '0')
.replace('{GDPR_CONSENT}', consent.gdprConsent || '')
.replace('{US_PRIVACY}', consent.usPrivacy || '')
}

3. Use Secure Connections

Always use HTTPS for tracking URLs:

function validateTrackingUrl(url) {
if (!url.startsWith('https://')) {
console.error('Tracking URL must use HTTPS:', url)
return false
}
return true
}

Error Handling

1. Handle Tracking Failures

Don't block rendering if tracking fails:

async function fireTrackingSafely(url) {
try {
await fetch(url, {
method: 'GET',
mode: 'no-cors',
timeout: 5000,
})
} catch (error) {
console.error('Tracking failed:', error)
// Don't throw - tracking failure shouldn't break ad rendering
}
}

2. Retry Failed Tracking

Implement retry logic for critical tracking:

async function fireTrackingWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
await fetch(url, { method: 'GET', mode: 'no-cors' })
return // Success
} catch (error) {
if (i === maxRetries - 1) {
console.error('Tracking failed after retries:', error)
}
await sleep(Math.pow(2, i) * 1000) // Exponential backoff
}
}
}

3. Queue Offline Tracking

Queue tracking events when offline:

const trackingQueue = []

async function fireTrackingWithQueue(url) {
if (!navigator.onLine) {
trackingQueue.push(url)
return
}

await fireTrackingSafely(url)
}

// Flush queue when back online
window.addEventListener('online', async () => {
while (trackingQueue.length > 0) {
const url = trackingQueue.shift()
await fireTrackingSafely(url)
}
})

Best Practices

1. Fire Tracking in Correct Order

async function handleAdDisplay(bid, manifest) {
// 1. Fire win notification (burl)
await fireOpenRTBTracking(bid.burl, auctionPrice)

// 2. Render creative
await renderCreative(manifest)

// 3. Fire impression notifications
await fireOpenRTBTracking(bid.nurl)
await fireAdCPTracking(manifest, 'impression')

// 4. Setup viewability tracking
setupViewabilityTracking(element, manifest)

// 5. Setup click tracking
setupClickTracking(element, manifest)
}

2. Always Include Cachebuster

Prevent caching of tracking pixels:

const url = tracker.url.replace('{CACHEBUSTER}', Date.now())

3. Use Image Pixels for Reliability

For critical tracking, use image pixels:

function fireTrackingPixel(url) {
const img = new Image()
img.src = url
// Image will fire even if page unloads
}

4. Log Tracking Events

Log tracking for debugging:

function fireTracking(url, eventType) {
console.log(`Firing ${eventType} tracking:`, url)
fetch(url, { method: 'GET', mode: 'no-cors' })
}

5. Validate Tracking URLs

Validate URLs before firing:

function isValidTrackingUrl(url) {
try {
const parsed = new URL(url)
return parsed.protocol === 'https:'
} catch {
return false
}
}

Tracking Comparison

AspectOpenRTB Tracking (burl/nurl)AdCP Asset Tracking
PurposeBilling, win confirmationCreative performance analytics
GranularityBid-levelCreative-level
EventsWin, impressionImpression, click, viewability
Macros${AUCTION_PRICE}Universal macros
RequiredYes (for billing)Optional
Fired ByPublisherPublisher
Use CaseBilling reconciliationCampaign optimization

Next Steps