Takaisin blogiin
Technical

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

Sarah Chen, Privacy EngineerNovember 10, 202513 min lukuaika
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

Kirjoittaja GetCookiesissa, erikoistunut tietosuojavaatimustenmukaisuuteen, hyväksyntähallintaan ja digitaalisen markkinoinnin optimointiin.

Valmis yksinkertaistamaan evästehyväksyntää?

GetCookies tekee GDPR:n, CCPA:n ja maailmanlaajuisen tietosuojavaatimustenmukaisuuden vaivattomaksi. Aloita tänään.