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
}
2. Include Consent Signals
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
| Aspect | OpenRTB Tracking (burl/nurl) | AdCP Asset Tracking |
|---|---|---|
| Purpose | Billing, win confirmation | Creative performance analytics |
| Granularity | Bid-level | Creative-level |
| Events | Win, impression | Impression, click, viewability |
| Macros | ${AUCTION_PRICE} | Universal macros |
| Required | Yes (for billing) | Optional |
| Fired By | Publisher | Publisher |
| Use Case | Billing reconciliation | Campaign optimization |
Next Steps
- Creative Manifests - Manifest structure
- Format Support - Format declaration
- OpenRTB Response Format - Response structure