TLDR: Facebook CAPI improves measurement accuracy but doesn't exempt you from consent—proper integration is critical.
Read full summary
Technical guide to consent-compliant Facebook Conversions API implementation. Covers consent signal passing, event deduplication, data minimization, and maintaining measurement while respecting user choices.
*Summary by Claude AI*
---
title: "Facebook Conversions API (CAPI) and Consent: Complete Implementation Guide 2025"
slug: "facebook-capi-consent-implementation-guide"
excerpt: "Learn how to implement Facebook Conversions API while respecting user consent. This guide covers GDPR-compliant server-side tracking, event deduplication, consent state management, and best practices for maximizing data quality without violating privacy laws."
category: "Server-Side Tracking"
tags: ["Facebook CAPI", "Meta", "Server-Side Tracking", "GDPR", "Consent Management", "Conversions API"]
publishedAt: "2025-01-21"
readTime: "21 min read"
---
# Facebook Conversions API (CAPI) and Consent: Complete Implementation Guide
Moving tracking from the browser to the server doesn't eliminate consent requirements. Facebook's Conversions API (CAPI) is a powerful tool for maintaining measurement accuracy as third-party cookies decline, but many organizations mistakenly believe server-side tracking bypasses GDPR. This comprehensive guide explains how to implement CAPI properly while respecting user consent choices.
## Understanding Facebook Conversions API
The Conversions API allows you to send web events directly from your server to Facebook's servers. Unlike the Facebook Pixel (browser-based), CAPI events aren't blocked by ad blockers, ITP, or third-party cookie restrictions. However, this doesn't mean you can track users without consent.
### Why CAPI Still Requires Consent
The misconception that server-side tracking doesn't require consent stems from confusion about where the data processing occurs. GDPR doesn't care whether processing happens in the browser or on a server—it cares about whether personal data is being processed.
CAPI events typically include:
- Email addresses (hashed)
- Phone numbers (hashed)
- IP addresses
- User agent strings
- Click IDs (fbc, fbp)
- Customer IDs
All of these constitute personal data under GDPR. Processing them without a legal basis—whether via browser pixel or server API—violates the regulation.
### CAPI Event Structure
```javascript
// Standard CAPI event structure
const capiEvent = {
event_name: 'Purchase',
event_time: Math.floor(Date.now() / 1000),
event_id: 'evt_' + generateUUID(), // For deduplication
event_source_url: 'https://shop.example.com/checkout/success',
action_source: 'website',
user_data: {
// Always hash PII before sending
em: hashSHA256(email.toLowerCase().trim()),
ph: hashSHA256(phone.replace(/\D/g, '')),
fn: hashSHA256(firstName.toLowerCase().trim()),
ln: hashSHA256(lastName.toLowerCase().trim()),
ct: hashSHA256(city.toLowerCase().trim()),
st: hashSHA256(state.toLowerCase().trim()),
zp: hashSHA256(zipCode.trim()),
country: hashSHA256(country.toLowerCase().trim()),
// Browser identifiers
client_ip_address: clientIp,
client_user_agent: userAgent,
fbc: fbClickId, // Facebook click ID from URL parameter
fbp: fbBrowserId, // Facebook browser ID from cookie
// Your identifiers
external_id: hashSHA256(customerId)
},
custom_data: {
currency: 'USD',
value: 99.99,
content_ids: ['SKU-12345'],
content_type: 'product',
contents: [{
id: 'SKU-12345',
quantity: 1,
item_price: 99.99
}],
num_items: 1
}
};
```
## Implementing Consent-Aware CAPI
The key to compliant CAPI implementation is conditioning event transmission on consent state. Here's a complete implementation:
### Consent Manager Integration
```typescript
// Complete consent-aware CAPI implementation
class ConsentAwareCAPI {
private pixelId: string;
private accessToken: string;
private apiVersion: string = 'v18.0';
private baseUrl: string;
constructor(pixelId: string, accessToken: string) {
this.pixelId = pixelId;
this.accessToken = accessToken;
this.baseUrl = `https://graph.facebook.com/${this.apiVersion}/${this.pixelId}/events`;
}
// Main method to send events with consent checking
async sendEvent(event: CAPIEvent, consentState: ConsentState): Promise {
// Check if we have marketing consent
if (!this.hasMarketingConsent(consentState)) {
console.log('CAPI event blocked: No marketing consent');
return {
success: false,
blocked: true,
reason: 'no_consent'
};
}
// Determine data quality based on consent level
const processedEvent = this.processEventWithConsent(event, consentState);
try {
const response = await this.sendToFacebook(processedEvent);
return {
success: true,
blocked: false,
fbResponse: response
};
} catch (error) {
console.error('CAPI send error:', error);
return {
success: false,
blocked: false,
error: error.message
};
}
}
private hasMarketingConsent(consent: ConsentState): boolean {
// TCF-based consent check
if (consent.tcfData) {
// Facebook's vendor ID in TCF is 755
const facebookVendorConsent = consent.tcfData.vendor?.consents?.[755];
const purposeConsent = consent.tcfData.purpose?.consents;
// Need Purpose 1 (storage), 3 (ad profiles), 4 (personalized ads)
return facebookVendorConsent &&
purposeConsent?.[1] &&
purposeConsent?.[3] &&
purposeConsent?.[4];
}
// Simple consent check
return consent.marketing === true;
}
private processEventWithConsent(event: CAPIEvent, consent: ConsentState): CAPIEvent {
const processedEvent = { ...event };
// If analytics-only consent (no marketing), limit data
if (consent.analytics && !consent.marketing) {
// Remove user identifiers, keep aggregates only
delete processedEvent.user_data.em;
delete processedEvent.user_data.ph;
delete processedEvent.user_data.external_id;
processedEvent.user_data.fbp = null;
processedEvent.user_data.fbc = null;
}
// Add consent metadata
processedEvent.data_processing_options = this.getDataProcessingOptions(consent);
return processedEvent;
}
private getDataProcessingOptions(consent: ConsentState): string[] {
// Limited Data Use (LDU) for California/US privacy laws
if (consent.region === 'US-CA' && !consent.doNotSell) {
return ['LDU'];
}
// No restrictions if full consent
return [];
}
private async sendToFacebook(event: CAPIEvent): Promise {
const payload = {
data: [event],
access_token: this.accessToken,
test_event_code: process.env.NODE_ENV === 'development' ? 'TEST12345' : undefined
};
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error?.message || 'CAPI request failed');
}
return response.json();
}
}
interface CAPIEvent {
event_name: string;
event_time: number;
event_id: string;
event_source_url: string;
action_source: string;
user_data: UserData;
custom_data?: CustomData;
data_processing_options?: string[];
}
interface ConsentState {
essential: boolean;
analytics: boolean;
marketing: boolean;
tcfData?: TCFData;
region?: string;
doNotSell?: boolean;
}
interface CAPIResponse {
success: boolean;
blocked: boolean;
reason?: string;
error?: string;
fbResponse?: any;
}
```
### Server-Side Consent State Management
The challenge with server-side tracking is knowing the user's consent state. Here are several approaches:
```typescript
// Approach 1: Pass consent state with each request
class ConsentStateManager {
// Client-side: Store consent in cookie and send with requests
static storeConsentClient(): void {
const consent = window.CookieConsent?.getConsent() || {};
// Store in first-party cookie for server access
document.cookie = `consent_state=${encodeURIComponent(JSON.stringify({
marketing: consent.marketing || false,
analytics: consent.analytics || false,
timestamp: Date.now()
}))}; path=/; max-age=31536000; SameSite=Lax`;
}
// Server-side: Read consent from request
static getConsentFromRequest(req: Request): ConsentState {
const consentCookie = req.cookies?.consent_state;
if (!consentCookie) {
// No consent recorded = no processing
return {
essential: true,
analytics: false,
marketing: false
};
}
try {
const parsed = JSON.parse(decodeURIComponent(consentCookie));
// Validate timestamp (re-prompt if too old)
const maxAge = 365 * 24 * 60 * 60 * 1000; // 1 year
if (Date.now() - parsed.timestamp > maxAge) {
return {
essential: true,
analytics: false,
marketing: false
};
}
return {
essential: true,
analytics: parsed.analytics || false,
marketing: parsed.marketing || false
};
} catch {
return {
essential: true,
analytics: false,
marketing: false
};
}
}
}
// Approach 2: Database-backed consent for logged-in users
class DatabaseConsentManager {
private db: Database;
async getConsentForUser(userId: string): Promise {
const record = await this.db.query(
`SELECT analytics_consent, marketing_consent, consent_timestamp, consent_version
FROM user_consents
WHERE user_id = $1
ORDER BY consent_timestamp DESC
LIMIT 1`,
[userId]
);
if (!record) {
return this.getDefaultConsent();
}
// Check if consent version is current
if (record.consent_version !== this.getCurrentConsentVersion()) {
return this.getDefaultConsent(); // Need re-consent
}
return {
essential: true,
analytics: record.analytics_consent,
marketing: record.marketing_consent
};
}
async saveConsent(userId: string, consent: ConsentState): Promise {
await this.db.query(
`INSERT INTO user_consents
(user_id, analytics_consent, marketing_consent, consent_timestamp, consent_version, ip_address, user_agent)
VALUES ($1, $2, $3, NOW(), $4, $5, $6)`,
[
userId,
consent.analytics,
consent.marketing,
this.getCurrentConsentVersion(),
consent.ipAddress,
consent.userAgent
]
);
}
private getCurrentConsentVersion(): string {
return '2025.01.21'; // Update when privacy policy changes
}
private getDefaultConsent(): ConsentState {
return {
essential: true,
analytics: false,
marketing: false
};
}
}
```
## Event Deduplication: Browser Pixel + CAPI
When running both the Facebook Pixel (browser-side) and CAPI (server-side), you must deduplicate events to avoid double-counting conversions. Facebook uses the `event_id` field for this.
### Deduplication Strategy
```typescript
// Deduplication implementation
class CAPIDeduplication {
// Generate consistent event ID for both pixel and CAPI
static generateEventId(eventName: string, userData: any, customData: any): string {
// Create deterministic ID based on event properties
const components = [
eventName,
userData.external_id || '',
customData.order_id || customData.content_ids?.join(',') || '',
Math.floor(Date.now() / 60000) // Minute-level granularity
];
return 'evt_' + this.hashString(components.join('|'));
}
private static hashString(str: string): string {
// Simple hash for event ID (not for PII)
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash).toString(36);
}
}
// Browser-side implementation
class BrowserPixelWithDedup {
trackEvent(eventName: string, params: any): void {
// Check consent first
if (!this.hasMarketingConsent()) {
console.log('Pixel blocked: no consent');
return;
}
// Generate dedup event ID
const eventId = CAPIDeduplication.generateEventId(
eventName,
{ external_id: params.user_id },
params
);
// Fire pixel with event_id
fbq('track', eventName, params, { eventID: eventId });
// Store event ID for server-side to use
this.storeEventIdForServer(eventId, eventName);
}
private storeEventIdForServer(eventId: string, eventName: string): void {
// Store in session for server to access
const events = JSON.parse(sessionStorage.getItem('fb_events') || '[]');
events.push({ eventId, eventName, timestamp: Date.now() });
// Keep only last 50 events
if (events.length > 50) {
events.shift();
}
sessionStorage.setItem('fb_events', JSON.stringify(events));
}
private hasMarketingConsent(): boolean {
const consent = document.cookie.match(/consent_state=([^;]+)/);
if (consent) {
try {
const parsed = JSON.parse(decodeURIComponent(consent[1]));
return parsed.marketing === true;
} catch {
return false;
}
}
return false;
}
}
// Server-side implementation with dedup
class ServerCAPIWithDedup {
private capi: ConsentAwareCAPI;
async sendPurchaseEvent(order: Order, consent: ConsentState): Promise {
// Generate same event ID as browser would
const eventId = CAPIDeduplication.generateEventId(
'Purchase',
{ external_id: order.customerId },
{ order_id: order.id, content_ids: order.items.map(i => i.sku) }
);
const event: CAPIEvent = {
event_name: 'Purchase',
event_time: Math.floor(order.createdAt.getTime() / 1000),
event_id: eventId, // Same ID = deduplication
event_source_url: `https://shop.example.com/checkout/success?order=${order.id}`,
action_source: 'website',
user_data: await this.buildUserData(order),
custom_data: {
currency: order.currency,
value: order.total,
content_ids: order.items.map(i => i.sku),
content_type: 'product',
contents: order.items.map(i => ({
id: i.sku,
quantity: i.quantity,
item_price: i.price
})),
num_items: order.items.reduce((sum, i) => sum + i.quantity, 0),
order_id: order.id
}
};
await this.capi.sendEvent(event, consent);
}
private async buildUserData(order: Order): Promise {
return {
em: this.hashSHA256(order.email.toLowerCase().trim()),
ph: order.phone ? this.hashSHA256(order.phone.replace(/\D/g, '')) : undefined,
fn: this.hashSHA256(order.firstName.toLowerCase().trim()),
ln: this.hashSHA256(order.lastName.toLowerCase().trim()),
ct: order.city ? this.hashSHA256(order.city.toLowerCase().trim()) : undefined,
st: order.state ? this.hashSHA256(order.state.toLowerCase().trim()) : undefined,
zp: order.zipCode ? this.hashSHA256(order.zipCode.trim()) : undefined,
country: this.hashSHA256(order.country.toLowerCase().trim()),
external_id: this.hashSHA256(order.customerId),
client_ip_address: order.ipAddress,
client_user_agent: order.userAgent,
fbc: order.fbClickId,
fbp: order.fbBrowserId
};
}
private hashSHA256(value: string): string {
return crypto.createHash('sha256').update(value).digest('hex');
}
}
```
### Deduplication Window
Facebook deduplicates events with matching `event_id` within a 48-hour window. Both events must have:
- Same `event_id`
- Same `event_name`
- Same pixel ID
```javascript
// Handling deduplication failures
class DeduplicationMonitor {
async checkDeduplicationHealth(): Promise {
const stats = await this.fetchEventStats();
return {
totalEvents: stats.total,
browserOnlyEvents: stats.pixel_only,
serverOnlyEvents: stats.capi_only,
deduplicatedEvents: stats.matched,
deduplicationRate: stats.matched / stats.total,
recommendations: this.generateRecommendations(stats)
};
}
private generateRecommendations(stats: EventStats): string[] {
const recommendations = [];
// If server events aren't being deduped
if (stats.capi_only > stats.total * 0.1) {
recommendations.push(
'High server-only event rate. Check event_id generation consistency.'
);
}
// If browser events aren't being matched
if (stats.pixel_only > stats.total * 0.3) {
recommendations.push(
'Many pixel events without CAPI match. Server may not be receiving events.'
);
}
return recommendations;
}
}
```
## Handling Consent Changes
When a user changes their consent preferences, you must stop or start CAPI tracking accordingly:
```typescript
// Consent change handler
class ConsentChangeHandler {
private capi: ConsentAwareCAPI;
private pendingEvents: CAPIEvent[] = [];
// Called when user updates consent
async onConsentChange(newConsent: ConsentState, previousConsent: ConsentState): Promise {
// User granted marketing consent
if (newConsent.marketing && !previousConsent.marketing) {
await this.handleConsentGranted(newConsent);
}
// User revoked marketing consent
if (!newConsent.marketing && previousConsent.marketing) {
await this.handleConsentRevoked();
}
}
private async handleConsentGranted(consent: ConsentState): Promise {
// Process any buffered events (if implementing delayed consent)
for (const event of this.pendingEvents) {
await this.capi.sendEvent(event, consent);
}
this.pendingEvents = [];
// Log consent grant event (optional)
await this.capi.sendEvent({
event_name: 'ConsentGranted',
event_time: Math.floor(Date.now() / 1000),
event_id: 'consent_' + Date.now(),
event_source_url: window.location.href,
action_source: 'website',
user_data: {},
custom_data: {
consent_categories: ['marketing']
}
}, consent);
}
private async handleConsentRevoked(): Promise {
// Clear any pending events
this.pendingEvents = [];
// Optionally: Request data deletion from Facebook
// This requires separate implementation via deletion API
console.log('Marketing consent revoked. CAPI tracking stopped.');
}
// Buffer events when consent is unknown/pending
bufferEvent(event: CAPIEvent): void {
// Only buffer for short period (session)
this.pendingEvents.push(event);
// Limit buffer size
if (this.pendingEvents.length > 20) {
this.pendingEvents.shift();
}
}
}
```
## Integration with E-commerce Platforms
### Shopify CAPI Implementation
```typescript
// Shopify webhook handler for CAPI
class ShopifyCAPIIntegration {
private capi: ConsentAwareCAPI;
private consentManager: DatabaseConsentManager;
// Webhook handler for order creation
async handleOrderCreated(webhook: ShopifyOrderWebhook): Promise {
const order = webhook.order;
// Get consent state for this customer
const consent = await this.getConsentForOrder(order);
if (!consent.marketing) {
console.log(`Order ${order.id}: Skipping CAPI - no marketing consent`);
return;
}
// Build and send CAPI event
const event = this.buildPurchaseEvent(order);
await this.capi.sendEvent(event, consent);
}
private async getConsentForOrder(order: ShopifyOrder): Promise {
// Check customer-level consent
if (order.customer?.id) {
const customerConsent = await this.consentManager.getConsentForUser(
order.customer.id.toString()
);
if (customerConsent) return customerConsent;
}
// Check order-level consent (from checkout)
if (order.note_attributes) {
const consentAttr = order.note_attributes.find(
attr => attr.name === 'marketing_consent'
);
if (consentAttr) {
return {
essential: true,
analytics: true,
marketing: consentAttr.value === 'true'
};
}
}
// Check accepts_marketing field
if (order.customer?.accepts_marketing) {
// Note: accepts_marketing is for email, not necessarily ad tracking
// Be conservative and still require explicit consent
}
// Default: no consent
return {
essential: true,
analytics: false,
marketing: false
};
}
private buildPurchaseEvent(order: ShopifyOrder): CAPIEvent {
const customer = order.customer || {};
const address = order.billing_address || order.shipping_address || {};
return {
event_name: 'Purchase',
event_time: Math.floor(new Date(order.created_at).getTime() / 1000),
event_id: `shopify_${order.id}_${order.order_number}`,
event_source_url: `https://${process.env.SHOPIFY_DOMAIN}/checkout`,
action_source: 'website',
user_data: {
em: customer.email ? this.hash(customer.email.toLowerCase()) : undefined,
ph: address.phone ? this.hash(address.phone.replace(/\D/g, '')) : undefined,
fn: address.first_name ? this.hash(address.first_name.toLowerCase()) : undefined,
ln: address.last_name ? this.hash(address.last_name.toLowerCase()) : undefined,
ct: address.city ? this.hash(address.city.toLowerCase()) : undefined,
st: address.province_code ? this.hash(address.province_code.toLowerCase()) : undefined,
zp: address.zip ? this.hash(address.zip) : undefined,
country: address.country_code ? this.hash(address.country_code.toLowerCase()) : undefined,
external_id: customer.id ? this.hash(customer.id.toString()) : undefined
},
custom_data: {
currency: order.currency,
value: parseFloat(order.total_price),
content_ids: order.line_items.map(item => item.sku || item.product_id.toString()),
content_type: 'product',
contents: order.line_items.map(item => ({
id: item.sku || item.product_id.toString(),
quantity: item.quantity,
item_price: parseFloat(item.price)
})),
num_items: order.line_items.reduce((sum, item) => sum + item.quantity, 0),
order_id: order.order_number.toString()
}
};
}
private hash(value: string): string {
return crypto.createHash('sha256').update(value).digest('hex');
}
}
```
### WooCommerce CAPI Implementation
```php
pixel_id = get_option('fb_pixel_id');
$this->access_token = get_option('fb_capi_token');
// Hook into order completion
add_action('woocommerce_order_status_completed', [$this, 'send_purchase_event']);
add_action('woocommerce_thankyou', [$this, 'send_purchase_event_checkout']);
}
public function send_purchase_event($order_id) {
$order = wc_get_order($order_id);
if (!$order) return;
// Check consent
$consent = $this->get_consent_for_order($order);
if (!$consent['marketing']) {
error_log("Order $order_id: CAPI skipped - no marketing consent");
return;
}
$event = $this->build_purchase_event($order);
$this->send_to_facebook($event);
}
private function get_consent_for_order($order) {
// Check order meta for consent state
$consent_state = $order->get_meta('_cookie_consent_state');
if ($consent_state) {
$parsed = json_decode($consent_state, true);
return [
'essential' => true,
'analytics' => $parsed['analytics'] ?? false,
'marketing' => $parsed['marketing'] ?? false
];
}
// Check user meta if logged in
$user_id = $order->get_user_id();
if ($user_id) {
$user_consent = get_user_meta($user_id, 'marketing_consent', true);
if ($user_consent) {
return [
'essential' => true,
'analytics' => true,
'marketing' => $user_consent === 'yes'
];
}
}
// Default: no marketing consent
return [
'essential' => true,
'analytics' => false,
'marketing' => false
];
}
private function build_purchase_event($order) {
$items = [];
foreach ($order->get_items() as $item) {
$product = $item->get_product();
$items[] = [
'id' => $product->get_sku() ?: $product->get_id(),
'quantity' => $item->get_quantity(),
'item_price' => floatval($item->get_total())
];
}
return [
'event_name' => 'Purchase',
'event_time' => strtotime($order->get_date_created()),
'event_id' => 'woo_' . $order->get_id() . '_' . $order->get_order_number(),
'event_source_url' => $order->get_checkout_order_received_url(),
'action_source' => 'website',
'user_data' => [
'em' => hash('sha256', strtolower(trim($order->get_billing_email()))),
'ph' => hash('sha256', preg_replace('/\D/', '', $order->get_billing_phone())),
'fn' => hash('sha256', strtolower(trim($order->get_billing_first_name()))),
'ln' => hash('sha256', strtolower(trim($order->get_billing_last_name()))),
'ct' => hash('sha256', strtolower(trim($order->get_billing_city()))),
'st' => hash('sha256', strtolower(trim($order->get_billing_state()))),
'zp' => hash('sha256', trim($order->get_billing_postcode())),
'country' => hash('sha256', strtolower(trim($order->get_billing_country()))),
'external_id' => hash('sha256', $order->get_user_id() ?: $order->get_billing_email())
],
'custom_data' => [
'currency' => $order->get_currency(),
'value' => floatval($order->get_total()),
'content_ids' => array_column($items, 'id'),
'content_type' => 'product',
'contents' => $items,
'num_items' => array_sum(array_column($items, 'quantity')),
'order_id' => $order->get_order_number()
]
];
}
private function send_to_facebook($event) {
$url = "https://graph.facebook.com/v18.0/{$this->pixel_id}/events";
$response = wp_remote_post($url, [
'body' => json_encode([
'data' => [$event],
'access_token' => $this->access_token
]),
'headers' => [
'Content-Type' => 'application/json'
]
]);
if (is_wp_error($response)) {
error_log('CAPI error: ' . $response->get_error_message());
}
}
}
// Hook consent state into order
add_action('woocommerce_checkout_create_order', function($order, $data) {
if (isset($_COOKIE['consent_state'])) {
$order->update_meta_data('_cookie_consent_state', sanitize_text_field($_COOKIE['consent_state']));
}
}, 10, 2);
```
## Testing CAPI Implementation
Facebook provides test event tools to verify your implementation:
```typescript
// CAPI testing utilities
class CAPITester {
private testEventCode: string;
private capi: ConsentAwareCAPI;
constructor(testEventCode: string) {
this.testEventCode = testEventCode;
}
// Send test event
async sendTestEvent(): Promise {
const testEvent: CAPIEvent = {
event_name: 'Purchase',
event_time: Math.floor(Date.now() / 1000),
event_id: 'test_' + Date.now(),
event_source_url: 'https://example.com/test',
action_source: 'website',
user_data: {
em: this.hash('
[email protected]'),
external_id: this.hash('test_user_123')
},
custom_data: {
currency: 'USD',
value: 99.99,
content_ids: ['TEST-SKU-001'],
content_type: 'product'
}
};
// Send with test event code
const response = await this.sendWithTestCode(testEvent);
return {
success: response.success,
eventId: testEvent.event_id,
message: response.message,
viewInEventsManager: `https://www.facebook.com/events_manager2/list/pixel/${this.capi.pixelId}/test_events`
};
}
// Verify deduplication
async testDeduplication(): Promise {
const eventId = 'dedup_test_' + Date.now();
// Send via browser simulation
const browserResult = await this.simulateBrowserPixel(eventId);
// Send via CAPI
const capiResult = await this.sendCAPIWithEventId(eventId);
return {
browserSent: browserResult.success,
capiSent: capiResult.success,
eventId: eventId,
checkDeduplication: 'Check Events Manager in 5-10 minutes to verify single event'
};
}
// Test consent blocking
async testConsentBlocking(): Promise {
// Test with no consent
const noConsentResult = await this.capi.sendEvent(
this.createTestEvent('no_consent_test'),
{ essential: true, analytics: false, marketing: false }
);
// Test with consent
const withConsentResult = await this.capi.sendEvent(
this.createTestEvent('with_consent_test'),
{ essential: true, analytics: true, marketing: true }
);
return {
noConsent: {
blocked: noConsentResult.blocked,
expected: true,
pass: noConsentResult.blocked === true
},
withConsent: {
blocked: withConsentResult.blocked,
expected: false,
pass: withConsentResult.blocked === false
}
};
}
private hash(value: string): string {
return crypto.createHash('sha256').update(value).digest('hex');
}
private createTestEvent(name: string): CAPIEvent {
return {
event_name: 'TestEvent',
event_time: Math.floor(Date.now() / 1000),
event_id: name + '_' + Date.now(),
event_source_url: 'https://example.com/test',
action_source: 'website',
user_data: { em: this.hash('
[email protected]') },
custom_data: { test_name: name }
};
}
}
```
## Data Quality Optimization
To maximize CAPI effectiveness while respecting consent:
```typescript
// Data quality optimization
class CAPIDataQuality {
// Score event data quality
scoreEventQuality(event: CAPIEvent): DataQualityScore {
const scores = {
user_data: this.scoreUserData(event.user_data),
custom_data: this.scoreCustomData(event.custom_data),
event_data: this.scoreEventMetadata(event)
};
const total = (scores.user_data + scores.custom_data + scores.event_data) / 3;
return {
total,
breakdown: scores,
recommendations: this.getRecommendations(scores)
};
}
private scoreUserData(userData: UserData): number {
const fields = ['em', 'ph', 'fn', 'ln', 'ct', 'st', 'zp', 'country', 'external_id', 'fbc', 'fbp'];
const filled = fields.filter(f => userData[f]).length;
return filled / fields.length;
}
private scoreCustomData(customData: CustomData): number {
const required = ['value', 'currency'];
const optional = ['content_ids', 'content_type', 'contents', 'num_items'];
const requiredScore = required.filter(f => customData[f] !== undefined).length / required.length;
const optionalScore = optional.filter(f => customData[f] !== undefined).length / optional.length;
return requiredScore * 0.7 + optionalScore * 0.3;
}
private scoreEventMetadata(event: CAPIEvent): number {
let score = 1;
if (!event.event_id) score -= 0.3; // Deduplication impossible
if (!event.event_source_url) score -= 0.2;
if (event.action_source !== 'website') score -= 0.1;
return Math.max(0, score);
}
private getRecommendations(scores: any): string[] {
const recs = [];
if (scores.user_data < 0.5) {
recs.push('Collect more user identifiers (email, phone) to improve match rates');
}
if (scores.user_data < 0.3) {
recs.push('Critical: Include at least email hash for event matching');
}
if (scores.custom_data < 0.7) {
recs.push('Add content_ids and contents array for better attribution');
}
return recs;
}
}
```
## The Key Principles
Facebook CAPI is a powerful tool for maintaining measurement accuracy, but it doesn't exempt you from consent requirements. The key principles for compliant implementation are:
1. **Always check consent** before sending CAPI events
2. **Use consistent event IDs** for proper deduplication with browser pixel
3. **Store consent state server-side** so webhooks can check before sending
4. **Hash all PII** before transmission (Facebook requirement)
5. **Handle consent changes** by stopping/starting tracking accordingly
6. **Test thoroughly** using Facebook's test event tools
By following this guide, you can leverage CAPI's benefits—resistance to ad blockers, improved data quality, and better attribution—while fully respecting user privacy choices. Remember: server-side tracking is not a consent loophole; it's a technical architecture that still requires legal basis for processing personal data.