블로그로 돌아가기
Technical

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

GetCookies 팀November 10, 202513분 소요
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

// 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

// 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<CAPIResponse> {
        // 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<any> {
        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:

// 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<ConsentState> {
        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<void> {
        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

// 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<void> {
        // 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<UserData> {
        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
// Handling deduplication failures
class DeduplicationMonitor {
    async checkDeduplicationHealth(): Promise<DeduplicationReport> {
        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:

// Consent change handler
class ConsentChangeHandler {
    private capi: ConsentAwareCAPI;
    private pendingEvents: CAPIEvent[] = [];

    // Called when user updates consent
    async onConsentChange(newConsent: ConsentState, previousConsent: ConsentState): Promise<void> {
        // 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<void> {
        // 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<void> {
        // 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

// Shopify webhook handler for CAPI
class ShopifyCAPIIntegration {
    private capi: ConsentAwareCAPI;
    private consentManager: DatabaseConsentManager;

    // Webhook handler for order creation
    async handleOrderCreated(webhook: ShopifyOrderWebhook): Promise<void> {
        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<ConsentState> {
        // 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
// WooCommerce CAPI implementation with consent checking

class WooCommerce_CAPI_Integration {
    private $pixel_id;
    private $access_token;

    public function __construct() {
        $this->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:

// CAPI testing utilities
class CAPITester {
    private testEventCode: string;
    private capi: ConsentAwareCAPI;

    constructor(testEventCode: string) {
        this.testEventCode = testEventCode;
    }

    // Send test event
    async sendTestEvent(): Promise<TestResult> {
        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<DeduplicationTestResult> {
        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<ConsentTestResult> {
        // 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:

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

G

GetCookies 팀

노르웨이 오슬로 Getia AS의 GetCookies 팀이 작성하고 관리합니다. 이 글은 쿠키 동의에 관한 일반 정보이며 법률 자문이 아닙니다.

내 사이트를 무료로 스캔하세요

무료 플랜은 사이트 1개에 배너를 표시하고 매월 쿠키 스캔 3회를 제공합니다. 카드 등록도, 영업 전화도 없습니다.