Back to Blog
Technical

Facebook CAPI (Conversions API) and Consent: The Missing Link

Sarah Chen, Privacy EngineerNovember 10, 202513 min read
FacebookCAPIServer-SideMeta

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.
S

Sarah Chen, Privacy Engineer

Contributing writer at GetCookies, specializing in privacy compliance, consent management, and digital marketing optimization.

Ready to Simplify Cookie Consent?

GetCookies makes GDPR, CCPA, and global privacy compliance effortless. Get started today.