Tracking Methods Overview
AdRoll supports two primary tracking approaches: client-side (JavaScript pixel in browser) and server-side (API calls from your backend). Each has trade-offs in implementation complexity, data accuracy, privacy compliance, and ad blocker resilience. Most implementations use a hybrid approach combining both methods for optimal coverage and accuracy.
Quick Comparison
| Aspect | Client-Side (Pixel) | Server-Side (API) |
|---|---|---|
| Implementation | Simple (paste snippet) | Complex (backend integration) |
| Ad blocker impact | High (30-40% blocked) | None (bypasses blockers) |
| Data accuracy | Lower (blockers, ITP) | Higher (all events captured) |
| User context | Full (IP, user agent, referrer) | Limited (must pass manually) |
| Cookie access | Automatic | Manual (must sync) |
| Real-time data | Yes | Yes |
| Privacy controls | Browser-dependent | Full control |
| Setup time | 5-30 minutes | 1-4 hours |
Client-Side Tracking (JavaScript Pixel)
How It Works
JavaScript pixel loads in user's browser and:
- Sets first-party cookies to identify user
- Captures pageviews, clicks, and conversions
- Sends data to AdRoll servers via HTTP requests
- Automatically collects browser context (IP, user agent, referrer)
Implementation
<!-- Paste in <head> of all pages -->
<script type="text/javascript">
adroll_adv_id = "ABC123XYZ";
adroll_pix_id = "DEF456GHI";
adroll_version = "2.0";
(function(w, d, e, o, a) {
w.__adroll_loaded = true;
w.adroll = w.adroll || [];
w.adroll.f = [ 'setProperties', 'identify', 'track' ];
var roundtripUrl = "https://s.adroll.com/j/" + adroll_adv_id + "/roundtrip.js";
for (a = 0; a < w.adroll.f.length; a++) {
w.adroll[w.adroll.f[a]] = w.adroll[w.adroll.f[a]] || (function(n) {
return function() {
w.adroll.push([ n, arguments ])
}
})(w.adroll.f[a])
}
e = d.createElement('script');
o = d.getElementsByTagName('script')[0];
e.async = 1;
e.src = roundtripUrl;
o.parentNode.insertBefore(e, o);
})(window, document);
</script>
Track events:
// Purchase conversion
adroll.track("purchase", {
order_id: "ORD-123",
conversion_value: 99.99,
currency: "USD"
});
// Product pageview
adroll.track("pageView", {
product_id: "SKU-001",
price: 49.99
});
Advantages
1. Simple implementation:
- Copy-paste code snippet
- No backend changes required
- Works with any website platform
- Automatically collects user context
2. Automatic data collection:
// Pixel automatically captures:
- IP address → geolocation
- User agent → device/browser
- Referrer → traffic source
- Cookies → user identification
- Page URL → content context
3. Real-time audience building:
- Users enter audiences immediately
- No data processing delay
- Dynamic remarketing works instantly
4. E-commerce integrations:
- Shopify, WooCommerce apps use client-side tracking
- Product catalog syncs automatically
- Zero backend code required
Disadvantages
1. Ad blocker impact (30-40% data loss):
// Ad blockers (uBlock Origin, AdBlock Plus) block:
- Script loading from s.adroll.com
- HTTP requests to d.adroll.com
- Cookie setting
Result: 30-40% of users not tracked
2. Browser privacy features:
- Safari ITP: Limits cookie lifespan to 7 days
- Firefox ETP: Blocks known tracking domains
- Chrome Privacy Sandbox: Future restrictions coming
3. Client-side performance impact:
// Pixel adds to page load:
- Initial script: ~15KB (gzipped)
- Additional requests: 2-3 per pageview
- Minimal but measurable performance cost
4. Depends on user's browser:
// Fails if:
- JavaScript disabled (~0.2% of users)
- Network errors during script load
- Slow connections timeout before pixel loads
Server-Side Tracking (API)
How It Works
Your backend server sends events directly to AdRoll API:
- User completes action (purchase, sign-up, etc.)
- Your server sends event to AdRoll API
- AdRoll processes event and attributes to user
- No browser-side JavaScript required
Implementation
API Endpoint:
POST https://services.adroll.com/api/v1/track
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
Request body:
{
"advertiser_id": "ABC123XYZ",
"pixel_id": "DEF456GHI",
"event": "purchase",
"user_id": "hashed_email_or_cookie_id",
"event_id": "unique_event_id",
"properties": {
"order_id": "ORD-123",
"conversion_value": 99.99,
"currency": "USD",
"products": [
{
"product_id": "SKU-001",
"quantity": 1,
"price": 99.99
}
]
},
"timestamp": 1640000000,
"user_data": {
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0..."
}
}
Example Implementations
Node.js (Express):
const axios = require('axios');
const crypto = require('crypto');
// Helper: Hash email for privacy
function hashEmail(email) {
return crypto.createHash('sha256')
.update(email.toLowerCase().trim())
.digest('hex');
}
// Track conversion server-side
async function trackAdRollConversion(orderData, userContext) {
try {
const response = await axios.post(
'https://services.adroll.com/api/v1/track',
{
advertiser_id: process.env.ADROLL_ADV_ID,
pixel_id: process.env.ADROLL_PIX_ID,
event: 'purchase',
user_id: hashEmail(orderData.customerEmail),
event_id: `order_${orderData.orderId}`,
properties: {
order_id: orderData.orderId,
conversion_value: orderData.total,
currency: orderData.currency,
products: orderData.items.map(item => ({
product_id: item.sku,
quantity: item.quantity,
price: item.price
}))
},
timestamp: Math.floor(Date.now() / 1000),
user_data: {
ip_address: userContext.ipAddress,
user_agent: userContext.userAgent
}
},
{
headers: {
'Authorization': `Bearer ${process.env.ADROLL_API_KEY}`,
'Content-Type': 'application/json'
}
}
);
console.log('AdRoll conversion tracked:', response.data);
return { success: true };
} catch (error) {
console.error('AdRoll API error:', error.response?.data || error.message);
return { success: false, error: error.message };
}
}
// Express route: Checkout completion
app.post('/api/checkout', async (req, res) => {
// 1. Process payment
const order = await processPayment(req.body);
// 2. Track conversion server-side
await trackAdRollConversion(
{
orderId: order.id,
customerEmail: order.email,
total: order.total,
currency: order.currency,
items: order.lineItems
},
{
ipAddress: req.ip,
userAgent: req.headers['user-agent']
}
);
// 3. Send response
res.json({ success: true, orderId: order.id });
});
Python (Flask):
import requests
import hashlib
import time
import os
def hash_email(email):
"""Hash email for privacy"""
return hashlib.sha256(email.lower().strip().encode()).hexdigest()
def track_adroll_conversion(order_data, user_context):
"""Send conversion to AdRoll API"""
url = "https://services.adroll.com/api/v1/track"
headers = {
"Authorization": f"Bearer {os.getenv('ADROLL_API_KEY')}",
"Content-Type": "application/json"
}
payload = {
"advertiser_id": os.getenv('ADROLL_ADV_ID'),
"pixel_id": os.getenv('ADROLL_PIX_ID'),
"event": "purchase",
"user_id": hash_email(order_data['customer_email']),
"event_id": f"order_{order_data['order_id']}",
"properties": {
"order_id": order_data['order_id'],
"conversion_value": order_data['total'],
"currency": order_data['currency'],
"products": [
{
"product_id": item['sku'],
"quantity": item['quantity'],
"price": item['price']
}
for item in order_data['items']
]
},
"timestamp": int(time.time()),
"user_data": {
"ip_address": user_context['ip_address'],
"user_agent": user_context['user_agent']
}
}
try:
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
print(f"AdRoll conversion tracked: {response.json()}")
return {"success": True}
except requests.exceptions.RequestException as e:
print(f"AdRoll API error: {e}")
return {"success": False, "error": str(e)}
# Flask route
@app.route('/api/checkout', methods=['POST'])
def checkout():
# Process payment
order = process_payment(request.json)
# Track server-side
track_adroll_conversion(
{
"order_id": order['id'],
"customer_email": order['email'],
"total": order['total'],
"currency": order['currency'],
"items": order['line_items']
},
{
"ip_address": request.remote_addr,
"user_agent": request.headers.get('User-Agent')
}
)
return jsonify({"success": True, "order_id": order['id']})
PHP:
<?php
function hashEmail($email) {
return hash('sha256', strtolower(trim($email)));
}
function trackAdRollConversion($orderData, $userContext) {
$url = 'https://services.adroll.com/api/v1/track';
$payload = [
'advertiser_id' => getenv('ADROLL_ADV_ID'),
'pixel_id' => getenv('ADROLL_PIX_ID'),
'event' => 'purchase',
'user_id' => hashEmail($orderData['customer_email']),
'event_id' => 'order_' . $orderData['order_id'],
'properties' => [
'order_id' => $orderData['order_id'],
'conversion_value' => $orderData['total'],
'currency' => $orderData['currency'],
'products' => array_map(function($item) {
return [
'product_id' => $item['sku'],
'quantity' => $item['quantity'],
'price' => $item['price']
];
}, $orderData['items'])
],
'timestamp' => time(),
'user_data' => [
'ip_address' => $userContext['ip_address'],
'user_agent' => $userContext['user_agent']
]
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . getenv('ADROLL_API_KEY'),
'Content-Type: application/json'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
error_log('AdRoll conversion tracked: ' . $response);
return ['success' => true];
} else {
error_log('AdRoll API error: ' . $response);
return ['success' => false, 'error' => $response];
}
}
// After order is completed
$order = processCheckout($_POST);
trackAdRollConversion(
[
'order_id' => $order['id'],
'customer_email' => $order['email'],
'total' => $order['total'],
'currency' => $order['currency'],
'items' => $order['line_items']
],
[
'ip_address' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT']
]
);
?>
Advantages
1. Ad blocker immunity:
Server-side tracking bypasses:
✓ Browser ad blockers (uBlock, AdBlock Plus)
✓ Safari ITP cookie restrictions
✓ Firefox Enhanced Tracking Protection
✓ DNS-based blocking (Pi-hole)
Result: 100% event capture (vs. 60-70% client-side)
2. Data accuracy:
- All conversions tracked (no browser blocking)
- No JavaScript errors or network failures
- Guaranteed event delivery
3. Privacy control:
// Full control over what data is sent
// Hash PII before sending
user_id: hashEmail(customer.email)
// Only send necessary fields
// Omit sensitive data (credit cards, SSNs)
4. Reduced client-side load:
- No JavaScript to download/execute
- Faster page loads
- Better mobile performance
Disadvantages
1. Implementation complexity:
Requires:
- Backend code changes
- API key management
- Error handling
- Manual user identification
- IP/user agent passing
2. User identification challenges:
// Client-side: Automatic cookie-based ID
// Server-side: Must manually track user
// Options:
1. Hash email (requires email collection)
2. Pass cookie ID from client to server
3. Use database user ID (AdRoll may not recognize)
3. Missing automatic context:
// Client-side automatically captures:
- Referrer URL
- Page URL
- Session data
- Device/browser info
// Server-side: Must manually collect and pass
user_data: {
ip_address: req.ip, // Manual
user_agent: req.headers['ua'], // Manual
referrer: req.headers['ref'] // Manual
}
4. Delayed pageview tracking:
Client-side: Tracks every pageview automatically
Server-side: Only tracks events you explicitly send
→ Server-side alone doesn't build "all visitors" audience
→ Need client-side pixel for pageview audience building
Hybrid Approach (Recommended)
Why Hybrid?
Combine client-side and server-side for best of both:
- Client-side: Pageviews, audience building, automatic context
- Server-side: Conversions, bypassing ad blockers, privacy control
Coverage comparison:
Client-side only: 60-70% conversion capture (ad blockers)
Server-side only: 100% conversions, but no pageview audiences
Hybrid: 100% conversions + full audience building
Implementation Strategy
1. Client-side pixel for pageviews:
<!-- Install pixel on all pages for audience building -->
<script type="text/javascript">
adroll_adv_id = "ABC123XYZ";
adroll_pix_id = "DEF456GHI";
/* ... pixel code ... */
</script>
<script>
// Track product pageviews
adroll.track("pageView", {
product_id: "SKU-001",
price: 49.99
});
</script>
2. Server-side API for conversions:
// On checkout completion (server-side)
await trackAdRollConversion({
order_id: "ORD-123",
conversion_value: 99.99,
currency: "USD"
});
3. Deduplication strategy:
Prevent counting same conversion twice (client + server):
// CLIENT-SIDE: Generate unique event ID
const eventId = 'evt_' + Date.now() + '_' + Math.random().toString(36);
// Send to AdRoll
adroll.track("purchase", {
order_id: "ORD-123",
conversion_value: 99.99,
currency: "USD",
event_id: eventId // Deduplication key
});
// Also send event_id to your server
fetch('/api/track-conversion', {
method: 'POST',
body: JSON.stringify({
order_id: "ORD-123",
event_id: eventId // Same ID
})
});
// SERVER-SIDE: Use same event_id in API call
trackAdRollConversion({
order_id: "ORD-123",
conversion_value: 99.99,
event_id: eventId, // SAME ID as client
// AdRoll deduplicates based on event_id
});
Deduplication behavior:
Client-side fires first: event_id = "evt_123"
Server-side fires 2s later: event_id = "evt_123" (same)
AdRoll result: 1 conversion (deduplicated)
Hybrid Architecture Example
// === CLIENT-SIDE (browser) ===
// 1. Load pixel for pageviews
<script src="adroll-pixel.js"></script>
// 2. Track product views automatically
adroll.track("pageView", {
product_id: currentProduct.id,
price: currentProduct.price
});
// 3. On checkout: Track client-side with event_id
async function completeCheckout(orderData) {
const eventId = generateEventId();
// Send to AdRoll (client-side)
adroll.track("purchase", {
order_id: orderData.id,
conversion_value: orderData.total,
event_id: eventId
});
// Send to server (for server-side tracking)
await fetch('/api/complete-order', {
method: 'POST',
body: JSON.stringify({
...orderData,
adroll_event_id: eventId // Pass event_id to server
})
});
}
// === SERVER-SIDE (backend) ===
app.post('/api/complete-order', async (req, res) => {
const order = req.body;
// 1. Save order to database
await db.orders.create(order);
// 2. Track server-side with SAME event_id
await trackAdRollConversion({
order_id: order.id,
conversion_value: order.total,
currency: order.currency,
event_id: order.adroll_event_id, // Same ID as client
user_id: hashEmail(order.customer_email),
user_data: {
ip_address: req.ip,
user_agent: req.headers['user-agent']
}
});
res.json({ success: true });
});
Result:
- If client-side succeeds: Conversion tracked immediately
- If client-side blocked: Server-side ensures conversion tracked
- If both succeed: AdRoll deduplicates via
event_id - Audience building: Client-side pixel builds audiences from pageviews
User Identification Strategies
Challenge
Server-side API requires user_id to attribute events:
// AdRoll needs to know: "Which user did this?"
user_id: "???" // What to use?
Option 1: Hashed Email (Best for Conversions)
// Use SHA-256 hashed email as user_id
const crypto = require('crypto');
function hashEmail(email) {
return crypto.createHash('sha256')
.update(email.toLowerCase().trim())
.digest('hex');
}
// Track conversion with hashed email
trackAdRollConversion({
user_id: hashEmail('customer@example.com'),
event: 'purchase',
// ...
});
Pros:
- Works across devices (same email, same ID)
- Privacy-friendly (hashed, not plain text)
- Matches AdRoll's email-based audiences
Cons:
- Only works when you have email (post-conversion)
- Can't track anonymous pageviews
Option 2: AdRoll Cookie ID (Best for Hybrid)
// CLIENT-SIDE: Get AdRoll's cookie ID
function getAdRollUserId() {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === '__adroll_fpc') {
return value; // AdRoll's user ID
}
}
return null;
}
// Send to server with checkout request
const adrollUserId = getAdRollUserId();
fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({
cart: cartData,
adroll_user_id: adrollUserId // Pass to server
})
});
// SERVER-SIDE: Use AdRoll's cookie ID
app.post('/api/checkout', async (req, res) => {
trackAdRollConversion({
user_id: req.body.adroll_user_id, // From client
event: 'purchase',
// ...
});
});
Pros:
- Matches AdRoll's existing user tracking
- Works for anonymous users (no email needed)
- Seamless hybrid integration
Cons:
- Requires client-side pixel to set cookie first
- Blocked if user has ad blockers (no cookie set)
Option 3: Database User ID (Fallback)
// Use your own user ID as fallback
trackAdRollConversion({
user_id: `db_user_${userId}`, // Prefix to avoid conflicts
event: 'purchase',
// ...
});
Pros:
- Always available (from your database)
- Works offline or without browser
Cons:
- AdRoll may not recognize ID (can't match to existing audiences)
- Doesn't sync with client-side pixel users
Recommended Strategy
// Cascading user ID strategy (best to worst)
function getAdRollUserId(user) {
// 1. Try AdRoll cookie ID (best for hybrid)
if (user.adroll_cookie_id) {
return user.adroll_cookie_id;
}
// 2. Use hashed email (good for conversions)
if (user.email) {
return hashEmail(user.email);
}
// 3. Fallback to database ID (last resort)
return `db_user_${user.id}`;
}
When to Use Each Approach
Use Client-Side Only When:
- ✓ Simple website with basic tracking needs
- ✓ E-commerce platform with app integration (Shopify, WooCommerce)
- ✓ No development resources for backend integration
- ✓ Ad blocker impact acceptable (30-40% data loss okay)
- ✓ Budget or time constraints prevent server-side build
Use Server-Side Only When:
- ✓ Mobile app (no browser/JavaScript)
- ✓ Backend-only processing (no frontend)
- ✓ Maximum data accuracy required
- ✓ Privacy regulations require server-side control
- ✓ High-value conversions (can't afford to miss any)
Use Hybrid When:
- ✓ Best accuracy needed (both pageviews and conversions)
- ✓ Want to bypass ad blockers for conversions
- ✓ Need audience building AND guaranteed conversion tracking
- ✓ Have development resources for backend integration
- ✓ Recommended for most e-commerce implementations
API Reference
Authentication
Get API key from AdRoll dashboard:
- Settings → API Access → Create API Key
- Copy key:
Bearer adroll_api_xxxxxxxxxxxxx - Store securely (environment variable, secrets manager)
Usage:
curl -X POST https://services.adroll.com/api/v1/track \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"advertiser_id": "ABC123", ...}'
Event Types
Purchase:
{
"event": "purchase",
"properties": {
"order_id": "ORD-123",
"conversion_value": 99.99,
"currency": "USD",
"products": [...]
}
}
Lead:
{
"event": "lead",
"properties": {
"segment_name": "contact_leads",
"conversion_value": 50
}
}
Page View:
{
"event": "pageView",
"properties": {
"product_id": "SKU-001",
"price": 49.99,
"url": "https://example.com/products/widget"
}
}
Add to Cart:
{
"event": "addToCart",
"properties": {
"product_id": "SKU-001",
"quantity": 1,
"price": 49.99
}
}
Rate Limits
- 100 requests per minute per API key
- 10,000 requests per day per advertiser
- Exceeding limits returns
429 Too Many Requests
Implement retry logic:
async function trackWithRetry(data, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await axios.post(API_URL, data, { headers });
return response.data;
} catch (error) {
if (error.response?.status === 429) {
// Rate limited, wait and retry
await sleep(2 ** i * 1000); // Exponential backoff
continue;
}
throw error; // Other errors, don't retry
}
}
throw new Error('Max retries exceeded');
}
Testing & Validation
Test Server-Side API
Manual test with curl:
curl -X POST https://services.adroll.com/api/v1/track \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"advertiser_id": "ABC123XYZ",
"pixel_id": "DEF456GHI",
"event": "purchase",
"user_id": "test_user_123",
"event_id": "test_event_'$(date +%s)'",
"properties": {
"order_id": "TEST-'$(date +%s)'",
"conversion_value": 10.00,
"currency": "USD"
},
"timestamp": '$(date +%s)'
}'
Expected response:
{
"success": true,
"event_id": "test_event_1640000000"
}
Verify in AdRoll Dashboard
- AdRoll → Reports → Conversions
- Filter by recent date
- Look for test order ID
- May take 24-48 hours to appear
Validate Deduplication
// Send same event twice with same event_id
const eventId = 'dedup_test_' + Date.now();
// First call
await trackAdRollConversion({
event_id: eventId,
order_id: "TEST-001",
conversion_value: 10.00
});
// Second call (should be deduplicated)
await trackAdRollConversion({
event_id: eventId, // SAME ID
order_id: "TEST-001",
conversion_value: 10.00
});
// Check dashboard: Should see 1 conversion, not 2
Best Practices
Security
1. Store API keys securely:
// WRONG - Hardcoded API key
const API_KEY = 'adroll_api_abc123xyz';
// CORRECT - Environment variable
const API_KEY = process.env.ADROLL_API_KEY;
// BEST - Secrets manager (AWS Secrets Manager, Vault)
const API_KEY = await secretsManager.getSecret('adroll_api_key');
2. Hash PII before sending:
// Hash email, don't send plain text
user_id: hashEmail(customer.email)
// Never send credit cards or SSNs
// ❌ properties: { credit_card: '4111...' }
3. Validate data before sending:
function validateConversion(data) {
if (!data.order_id) throw new Error('order_id required');
if (typeof data.conversion_value !== 'number') throw new Error('conversion_value must be number');
if (!['USD', 'EUR', 'GBP'].includes(data.currency)) throw new Error('Invalid currency');
// ... more validation
}
Error Handling
async function trackAdRollConversion(data) {
try {
const response = await axios.post(API_URL, data, { headers });
console.log('✓ AdRoll conversion tracked');
return { success: true };
} catch (error) {
// Log error but don't break checkout
console.error('AdRoll API error:', error.response?.data || error.message);
// Send to error monitoring (Sentry, Rollbar)
errorMonitor.capture(error, { context: 'adroll_tracking' });
// Continue with order processing
return { success: false, error: error.message };
}
}
// Don't let AdRoll tracking failure break checkout
const order = await createOrder(data);
await trackAdRollConversion(order); // Async, non-blocking
return { orderId: order.id }; // Return success regardless
Performance Optimization
1. Fire and forget (don't await):
// SLOW - Waits for AdRoll response before returning
await trackAdRollConversion(order);
res.json({ orderId: order.id });
// FAST - Fire tracking in background
trackAdRollConversion(order); // No await
res.json({ orderId: order.id }); // Return immediately
2. Queue for batch processing:
// Add to queue instead of real-time API call
await queue.add('adroll-conversion', {
order_id: order.id,
// ... data
});
// Worker processes queue in background
queue.process('adroll-conversion', async (job) => {
await trackAdRollConversion(job.data);
});
Next Steps
- Event Tracking - Client-side event implementation
- Install or Embed Tag - Client-side pixel setup
- Troubleshooting & Debugging - Fix API and pixel issues
- Integrations - Platform-specific implementations