← Back to Documentation
    DocumentationintegrationsFeb 11, 2025

    Meta Conversions API (CAPI) Setup Reference

    Complete technical reference for Meta Conversions API: architecture, event types, customer information parameters, implementation methods, deduplication, testing, and Aggregated Event Measurement.

    Overview

    The Meta Conversions API (CAPI) allows advertisers to send web, app, and offline events directly from their server to Meta. Unlike the browser-based Meta Pixel, CAPI is not affected by ad blockers, cookie restrictions, or browser privacy features such as ITP (Intelligent Tracking Prevention).

    Running both the Meta Pixel and CAPI simultaneously is the recommended setup. The Pixel captures in-browser signals (scroll depth, time on page, click interactions), while CAPI provides a reliable server-side data path that survives network interruptions and privacy controls.

    Architecture: Browser Pixel vs Server-Side CAPI

                             User's Browser
                            ┌──────────────────────────────┐
                            │  Meta Pixel (fbevents.js)     │
                            │  - Fires on page interaction  │
                            │  - Sets _fbp / _fbc cookies   │
                            │  - Sends event_id for dedup   │
                            └──────────┬───────────────────┘
                                       │  HTTPS (browser → Meta)
                                       ▼
                            ┌──────────────────────────────┐
                            │       Meta Servers            │
                            │  - Receives Pixel events      │
                            │  - Receives CAPI events       │
                            │  - Deduplicates via event_id  │
                            │  - Matches users (EMQ)        │
                            │  - Feeds ad optimization      │
                            └──────────────────────────────┘
                                       ▲
                                       │  HTTPS (server → Meta)
                            ┌──────────┴───────────────────┐
                            │  Your Server / GTM SS         │
                            │  - Conversions API endpoint   │
                            │  - POST /v21.0/{pixel_id}/    │
                            │    events                     │
                            │  - Hashes PII before sending  │
                            │  - Sends same event_id        │
                            └──────────────────────────────┘
    

    Why both matter:

    • Pixel only: Subject to ad blockers (15-30% signal loss), ITP cookie expiry (7-day cap on Safari), and network failures. Optimization data is incomplete.
    • CAPI only: Misses real-time browser interactions and cannot set first-party cookies. Loses behavioral micro-signals the Pixel captures.
    • Pixel + CAPI (recommended): Redundant data paths with deduplication. Meta receives events from whichever path succeeds. Typically recovers 10-25% of lost conversions. Required for highest Event Match Quality scores.

    Event Types

    Standard Events

    Standard events are predefined by Meta and enable automatic optimization, reporting, and audience building.

    Purchase

    Fired when a transaction is completed.

    ParameterTypeRequiredDescription
    valuefloatYesTotal transaction value
    currencystringYesISO 4217 currency code (e.g., USD, EUR)
    content_idsarray<string>RecommendedProduct IDs from your catalog
    content_typestringRecommendedproduct or product_group
    contentsarray<object>RecommendedArray of {id, quantity, item_price}
    content_namestringOptionalName of the product or page
    content_categorystringOptionalProduct category
    num_itemsintegerOptionalNumber of items in the order
    order_idstringOptionalYour internal order ID

    AddToCart

    Fired when a user adds an item to the shopping cart.

    ParameterTypeRequiredDescription
    valuefloatRecommendedValue of the item added
    currencystringRecommendedISO 4217 currency code
    content_idsarray<string>RecommendedProduct IDs
    content_typestringRecommendedproduct or product_group
    contentsarray<object>RecommendedArray of {id, quantity, item_price}
    content_namestringOptionalProduct name
    content_categorystringOptionalProduct category

    InitiateCheckout

    Fired when a user begins the checkout process.

    ParameterTypeRequiredDescription
    valuefloatRecommendedCart value at checkout initiation
    currencystringRecommendedISO 4217 currency code
    content_idsarray<string>RecommendedProduct IDs in cart
    content_typestringRecommendedproduct or product_group
    contentsarray<object>RecommendedArray of {id, quantity, item_price}
    num_itemsintegerOptionalNumber of items in cart

    CompleteRegistration

    Fired when a user completes a registration form.

    ParameterTypeRequiredDescription
    valuefloatOptionalValue assigned to the registration
    currencystringOptionalISO 4217 currency code
    content_namestringOptionalName of the registration form or page
    statusstringOptionalRegistration status (e.g., complete)

    Lead

    Fired when a user submits information indicating interest (e.g., form fill, quote request).

    ParameterTypeRequiredDescription
    valuefloatOptionalEstimated lead value
    currencystringOptionalISO 4217 currency code
    content_namestringOptionalName of the lead form or page
    content_categorystringOptionalLead category

    ViewContent

    Fired when a user views a key page (e.g., product page, landing page).

    ParameterTypeRequiredDescription
    valuefloatRecommendedValue of the content
    currencystringRecommendedISO 4217 currency code
    content_idsarray<string>RecommendedProduct or content IDs
    content_typestringRecommendedproduct or product_group
    content_namestringOptionalContent or product name
    content_categorystringOptionalContent category

    Search

    Fired when a user performs a search.

    ParameterTypeRequiredDescription
    search_stringstringRecommendedThe search query
    valuefloatOptionalValue assigned to the search
    currencystringOptionalISO 4217 currency code
    content_idsarray<string>OptionalIDs of search results shown
    content_categorystringOptionalCategory of search results

    AddPaymentInfo

    Fired when a user adds payment information during checkout.

    ParameterTypeRequiredDescription
    valuefloatOptionalCart value
    currencystringOptionalISO 4217 currency code
    content_idsarray<string>OptionalProduct IDs
    content_typestringOptionalproduct or product_group
    content_categorystringOptionalPayment method category

    AddToWishlist

    Fired when a user adds an item to a wishlist.

    ParameterTypeRequiredDescription
    valuefloatOptionalValue of the item
    currencystringOptionalISO 4217 currency code
    content_idsarray<string>OptionalProduct IDs
    content_namestringOptionalProduct name
    content_categorystringOptionalProduct category

    Contact

    Fired when a user initiates contact (phone call, SMS, email, chat).

    ParameterTypeRequiredDescription
    No required or recommended parameters. Send customer information parameters for matching.

    CustomizeProduct

    Fired when a user customizes a product (e.g., selects color, size, configuration).

    ParameterTypeRequiredDescription
    content_idsarray<string>OptionalProduct IDs
    content_typestringOptionalproduct or product_group
    content_namestringOptionalProduct name

    Donate

    Fired when a user makes a donation.

    ParameterTypeRequiredDescription
    valuefloatRecommendedDonation amount
    currencystringRecommendedISO 4217 currency code

    FindLocation

    Fired when a user searches for a physical location (store locator, branch finder).

    ParameterTypeRequiredDescription
    content_namestringOptionalLocation name or search query
    content_categorystringOptionalLocation type

    Schedule

    Fired when a user books an appointment or schedules a visit.

    ParameterTypeRequiredDescription
    content_namestringOptionalAppointment type
    content_categorystringOptionalService category

    StartTrial

    Fired when a user starts a free trial.

    ParameterTypeRequiredDescription
    valuefloatRecommendedPredicted trial value
    currencystringRecommendedISO 4217 currency code
    content_namestringOptionalTrial plan name

    SubmitApplication

    Fired when a user submits an application (loan, job, program).

    ParameterTypeRequiredDescription
    content_namestringOptionalApplication type
    content_categorystringOptionalApplication category

    Subscribe

    Fired when a user starts a paid subscription.

    ParameterTypeRequiredDescription
    valuefloatYesSubscription value
    currencystringYesISO 4217 currency code
    content_namestringOptionalSubscription plan name

    Custom Events

    Custom events are for actions not covered by standard events. They can be used for audience building and custom conversions but cannot power standard optimization objectives.

    Naming rules:

    • Maximum 40 characters
    • Use lowercase letters, numbers, and underscores only
    • Must not start with a number
    • Must not use reserved names (any standard event name, fb_, _fb, or Meta internal prefixes)
    • Examples: qualified_lead, demo_booked, pricing_page_scroll_50

    When to use custom events:

    • When no standard event maps to your conversion action
    • For micro-conversions used in audience building (e.g., video_75_percent, scroll_depth_90)
    • For offline or CRM-based events (e.g., sql_qualified, deal_closed)

    Customer Information Parameters

    Customer information parameters (also called user data) are sent alongside events to enable Meta to match events to user profiles. Higher match quality directly improves ad optimization and reporting accuracy.

    Available Parameters

    ParameterKeyDescriptionFormat Before Hashing
    EmailemUser email addressLowercase, trim whitespace
    PhonephPhone number with country codeDigits only, include country code (e.g., 14155551234)
    First NamefnUser first nameLowercase, trim, remove punctuation
    Last NamelnUser last nameLowercase, trim, remove punctuation
    CityctUser cityLowercase, trim, no punctuation, no spaces
    StatestState or province2-letter ANSI abbreviation, lowercase (e.g., ca)
    Zip CodezpPostal/zip codeTrim. US: 5-digit only (no +4). UK: area + district format
    CountrycountryCountry code2-letter ISO 3166-1 alpha-2, lowercase (e.g., us)
    Date of BirthdbDate of birthYYYYMMDD format (e.g., 19910526)
    GendergeGenderSingle letter: m or f
    External IDexternal_idYour internal user IDAny string. Hash recommended but not required

    Hashing Requirements

    All customer information parameters except external_id must be hashed using SHA-256 before sending to Meta via CAPI. The Meta Pixel hashes automatically; CAPI does not.

    Hashing procedure:

    1. Normalize the value (lowercase, trim whitespace, remove special characters per the format rules above)
    2. Hash using SHA-256
    3. Encode as a lowercase hexadecimal string (64 characters)
    // Node.js example
    const crypto = require('crypto');
    
    function hashForMeta(value) {
      if (!value) return null;
      const normalized = value.toString().trim().toLowerCase();
      return crypto.createHash('sha256').update(normalized).digest('hex');
    }
    
    // Examples
    hashForMeta('John');           // fn
    hashForMeta('john@example.com'); // em
    hashForMeta('14155551234');    // ph (digits only, with country code)
    
    # Python example
    import hashlib
    
    def hash_for_meta(value):
        if not value:
            return None
        normalized = str(value).strip().lower()
        return hashlib.sha256(normalized.encode('utf-8')).hexdigest()
    

    Common hashing mistakes:

    • Hashing before normalizing (uppercase letters in the hash input)
    • Double-hashing (hashing an already-hashed value)
    • Including whitespace in the hash input
    • Not including country code in phone numbers
    • Using MD5 instead of SHA-256

    Event Match Quality (EMQ)

    Event Match Quality is a score from 1 to 10 that Meta assigns to each event, indicating how well Meta could match the event to a user profile. Higher EMQ means better ad optimization.

    How EMQ is calculated:

    • Based on the number and quality of customer information parameters sent
    • Each additional parameter increases match probability
    • em (email) has the highest individual impact
    • Combinations matter: em + ph + fn + ln is significantly better than em alone

    Target scores:

    EMQ ScoreQualityImpact
    1-3PoorMost events unmatched; optimization severely limited
    4-5FairPartial matching; optimization suboptimal
    6-7GoodStrong matching; optimization works well
    8-10ExcellentNear-complete matching; full optimization potential

    How to improve EMQ:

    1. Send em (email) with every event where available -- single biggest impact
    2. Add ph (phone) as a second identifier
    3. Include fn + ln to disambiguate common emails
    4. Pass external_id to enable cross-device matching
    5. Forward fbp and fbc cookies from the browser to your server
    6. Ensure correct normalization and hashing -- malformed hashes count as missing data

    Implementation Methods

    1. Direct API Integration

    Send events directly from your application server to the Meta Conversions API endpoint.

    Endpoint: POST https://graph.facebook.com/v21.0/{pixel_id}/events

    Required headers:

    • Content-Type: application/json

    Required query parameters:

    • access_token -- System User token with ads_management or manage_pages permission

    Node.js Implementation

    const crypto = require('crypto');
    const https = require('https');
    
    const PIXEL_ID = 'YOUR_PIXEL_ID';
    const ACCESS_TOKEN = 'YOUR_ACCESS_TOKEN';
    const API_VERSION = 'v21.0';
    
    function hash(value) {
      if (!value) return undefined;
      return crypto.createHash('sha256')
        .update(value.toString().trim().toLowerCase())
        .digest('hex');
    }
    
    async function sendEvent(eventName, eventData, userData, eventId = null) {
      const payload = {
        data: [{
          event_name: eventName,
          event_time: Math.floor(Date.now() / 1000),
          event_id: eventId || crypto.randomUUID(),
          event_source_url: eventData.source_url,
          action_source: 'website',
          user_data: {
            em: hash(userData.email),
            ph: hash(userData.phone),
            fn: hash(userData.firstName),
            ln: hash(userData.lastName),
            ct: hash(userData.city),
            st: hash(userData.state),
            zp: hash(userData.zipCode),
            country: hash(userData.country),
            external_id: hash(userData.externalId),
            client_ip_address: userData.ipAddress,
            client_user_agent: userData.userAgent,
            fbp: userData.fbp,   // _fbp cookie value (not hashed)
            fbc: userData.fbc,   // _fbc cookie value (not hashed)
          },
          custom_data: eventData.customData || {},
        }],
      };
    
      const url = `https://graph.facebook.com/${API_VERSION}/${PIXEL_ID}/events?access_token=${ACCESS_TOKEN}`;
    
      const response = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });
    
      const result = await response.json();
    
      if (!response.ok) {
        throw new Error(`CAPI error: ${JSON.stringify(result)}`);
      }
    
      return result;
    }
    
    // Example: Send a Purchase event
    await sendEvent('Purchase', {
      source_url: 'https://example.com/checkout/thank-you',
      customData: {
        value: 99.99,
        currency: 'USD',
        content_ids: ['SKU-123', 'SKU-456'],
        content_type: 'product',
        order_id: 'ORDER-789',
        num_items: 2,
      },
    }, {
      email: 'john@example.com',
      phone: '14155551234',
      firstName: 'John',
      lastName: 'Doe',
      ipAddress: '203.0.113.50',
      userAgent: 'Mozilla/5.0 ...',
      fbp: 'fb.1.1612345678901.1234567890',
      fbc: 'fb.1.1612345678901.AbCdEfGhIjKl',
      externalId: 'user-12345',
    }, 'evt_purchase_abc123');
    

    Python Implementation

    import hashlib
    import time
    import uuid
    import requests
    
    PIXEL_ID = 'YOUR_PIXEL_ID'
    ACCESS_TOKEN = 'YOUR_ACCESS_TOKEN'
    API_VERSION = 'v21.0'
    
    def hash_value(value):
        if not value:
            return None
        normalized = str(value).strip().lower()
        return hashlib.sha256(normalized.encode('utf-8')).hexdigest()
    
    def send_event(event_name, event_data, user_data, event_id=None):
        url = f'https://graph.facebook.com/{API_VERSION}/{PIXEL_ID}/events'
    
        payload = {
            'data': [{
                'event_name': event_name,
                'event_time': int(time.time()),
                'event_id': event_id or str(uuid.uuid4()),
                'event_source_url': event_data.get('source_url'),
                'action_source': 'website',
                'user_data': {
                    'em': hash_value(user_data.get('email')),
                    'ph': hash_value(user_data.get('phone')),
                    'fn': hash_value(user_data.get('first_name')),
                    'ln': hash_value(user_data.get('last_name')),
                    'ct': hash_value(user_data.get('city')),
                    'st': hash_value(user_data.get('state')),
                    'zp': hash_value(user_data.get('zip_code')),
                    'country': hash_value(user_data.get('country')),
                    'external_id': hash_value(user_data.get('external_id')),
                    'client_ip_address': user_data.get('ip_address'),
                    'client_user_agent': user_data.get('user_agent'),
                    'fbp': user_data.get('fbp'),
                    'fbc': user_data.get('fbc'),
                },
                'custom_data': event_data.get('custom_data', {}),
            }],
            'access_token': ACCESS_TOKEN,
        }
    
        # Remove None values from user_data
        payload['data'][0]['user_data'] = {
            k: v for k, v in payload['data'][0]['user_data'].items()
            if v is not None
        }
    
        response = requests.post(url, json=payload)
        response.raise_for_status()
        return response.json()
    
    # Example: Send a Purchase event
    send_event('Purchase', {
        'source_url': 'https://example.com/checkout/thank-you',
        'custom_data': {
            'value': 99.99,
            'currency': 'USD',
            'content_ids': ['SKU-123', 'SKU-456'],
            'content_type': 'product',
            'order_id': 'ORDER-789',
            'num_items': 2,
        },
    }, {
        'email': 'john@example.com',
        'phone': '14155551234',
        'first_name': 'John',
        'last_name': 'Doe',
        'ip_address': '203.0.113.50',
        'user_agent': 'Mozilla/5.0 ...',
        'fbp': 'fb.1.1612345678901.1234567890',
        'fbc': 'fb.1.1612345678901.AbCdEfGhIjKl',
        'external_id': 'user-12345',
    }, event_id='evt_purchase_abc123')
    

    PHP Implementation

    <?php
    $pixelId = 'YOUR_PIXEL_ID';
    $accessToken = 'YOUR_ACCESS_TOKEN';
    $apiVersion = 'v21.0';
    
    function hashForMeta(?string $value): ?string {
        if (empty($value)) return null;
        $normalized = strtolower(trim($value));
        return hash('sha256', $normalized);
    }
    
    function sendEvent(string $eventName, array $eventData, array $userData, ?string $eventId = null): array {
        global $pixelId, $accessToken, $apiVersion;
    
        $payload = [
            'data' => [json_encode([[
                'event_name' => $eventName,
                'event_time' => time(),
                'event_id' => $eventId ?? bin2hex(random_bytes(16)),
                'event_source_url' => $eventData['source_url'] ?? null,
                'action_source' => 'website',
                'user_data' => array_filter([
                    'em' => hashForMeta($userData['email'] ?? null),
                    'ph' => hashForMeta($userData['phone'] ?? null),
                    'fn' => hashForMeta($userData['first_name'] ?? null),
                    'ln' => hashForMeta($userData['last_name'] ?? null),
                    'ct' => hashForMeta($userData['city'] ?? null),
                    'st' => hashForMeta($userData['state'] ?? null),
                    'zp' => hashForMeta($userData['zip_code'] ?? null),
                    'country' => hashForMeta($userData['country'] ?? null),
                    'external_id' => hashForMeta($userData['external_id'] ?? null),
                    'client_ip_address' => $userData['ip_address'] ?? null,
                    'client_user_agent' => $userData['user_agent'] ?? null,
                    'fbp' => $userData['fbp'] ?? null,
                    'fbc' => $userData['fbc'] ?? null,
                ]),
                'custom_data' => $eventData['custom_data'] ?? new \stdClass(),
            ]])],
            'access_token' => $accessToken,
        ];
    
        $url = "https://graph.facebook.com/{$apiVersion}/{$pixelId}/events";
    
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $response = curl_exec($ch);
        curl_close($ch);
    
        return json_decode($response, true);
    }
    

    2. GTM Server-Side Container

    Google Tag Manager server-side containers can forward events to Meta CAPI without custom server code.

    Setup steps:

    1. Deploy a GTM server-side container (Google Cloud Run, AWS, or other hosting)
    2. Create a Meta Conversions API tag in your server container
    3. Configure the tag:
      • API Access Token: Your system user token
      • Pixel ID: Your Meta Pixel ID
      • Action Source: website
      • Event Name: Map from the incoming event (e.g., GA4 purchase → Meta Purchase)
    4. Set up a trigger to fire on the relevant events forwarded from your web container
    5. Map user data parameters (email, phone, etc.) from the incoming event data
    6. Set the event_id to match the browser Pixel's eventID for deduplication

    GTM Server-Side Tag Configuration (key fields):

    Tag Type:          Meta Conversions API
    Pixel ID:          {{Meta Pixel ID}}
    API Access Token:  {{Meta CAPI Token}}
    Event Name:        {{Event Name}}
    Action Source:     website
    
    User Data:
      Email:           {{User Email - Hashed}}
      Phone:           {{User Phone - Hashed}}
      First Name:      {{User First Name - Hashed}}
      Last Name:       {{User Last Name - Hashed}}
      IP Address:      {{Client IP}}  (auto-populated in SS GTM)
      User Agent:      {{Client User Agent}}  (auto-populated)
      FBP:             {{FBP Cookie}}
      FBC:             {{FBC Cookie}}
    
    Event Parameters:
      Event ID:        {{Event ID}}  (must match browser Pixel eventID)
      Event Source URL: {{Page URL}}
      Value:           {{Event Value}}
      Currency:        {{Event Currency}}
      Content IDs:     {{Content IDs}}
      Content Type:    {{Content Type}}
    

    3. Partner Integrations

    Many e-commerce platforms offer built-in CAPI integrations:

    PlatformIntegration MethodNotes
    ShopifyNative integration in Meta channel appAutomatic dedup; sends Purchase, AddToCart, ViewContent, InitiateCheckout, Search, AddPaymentInfo
    WooCommerceFacebook for WooCommerce pluginRequires configuration; sends standard e-commerce events
    BigCommerceNative Meta integrationAutomatic CAPI via app
    Magento/Adobe CommerceMeta Business ExtensionServer-side events via extension
    WordPressPixelYourSite or official Meta pluginVaries by plugin quality

    Considerations for partner integrations:

    • Limited control over which parameters are sent
    • May not support all standard events
    • EMQ depends on what customer data the platform passes
    • Always verify in Events Manager that events are arriving with expected parameters

    4. Measurement Protocol Comparison

    The Meta Conversions API is conceptually similar to Google Analytics 4 Measurement Protocol, but with key differences:

    FeatureMeta CAPIGA4 Measurement Protocol
    Endpointgraph.facebook.com/v21.0/{pixel_id}/eventswww.google-analytics.com/mp/collect
    AuthSystem User access tokenAPI secret + measurement ID
    PII handlingSHA-256 hashing requiredNo PII accepted
    Dedupevent_id + event_name matchingNot applicable (no browser counterpart)
    BatchingUp to 1,000 events per requestUp to 25 events per request
    LatencyReal-time processingUp to 72 hours for reporting
    Primary useAd optimization + attributionAnalytics reporting

    Event Deduplication

    When running both the Meta Pixel and CAPI, the same conversion can be reported twice. Meta deduplicates events using the combination of event_id and event_name.

    How Deduplication Works

    1. Browser Pixel fires an event with eventID: "evt_abc123" and event_name: "Purchase"
    2. Your server sends the same event via CAPI with event_id: "evt_abc123" and event_name: "Purchase"
    3. Meta sees both events, matches on event_id + event_name, and counts only one conversion
    4. If only one path succeeds (e.g., ad blocker stops the Pixel), the other path still delivers the event

    Dedup Strategy

    // 1. Generate event_id in the browser BEFORE firing the Pixel event
    const eventId = crypto.randomUUID(); // e.g., "evt_abc123"
    
    // 2. Fire the Pixel event with this eventID
    fbq('track', 'Purchase', {
      value: 99.99,
      currency: 'USD',
      content_ids: ['SKU-123'],
    }, { eventID: eventId });
    
    // 3. Send the same eventId to your server (via form data, AJAX, or data layer)
    fetch('/api/track', {
      method: 'POST',
      body: JSON.stringify({
        event_name: 'Purchase',
        event_id: eventId,
        value: 99.99,
        currency: 'USD',
        content_ids: ['SKU-123'],
      }),
    });
    
    // 4. Server forwards to CAPI with the same event_id
    // (see Node.js / Python examples above)
    

    fbp and fbc Cookie Handling

    _fbp (Facebook Browser ID):

    • Set automatically by the Meta Pixel on first page load
    • Format: fb.1.{timestamp}.{random_number} (e.g., fb.1.1612345678901.1234567890)
    • Persists for 90 days as a first-party cookie
    • Read it server-side from the cookie header and pass as fbp in CAPI user_data
    • Do not hash -- send the raw cookie value

    _fbc (Facebook Click ID):

    • Set when a user clicks a Meta ad (contains the fbclid URL parameter)
    • Format: fb.1.{timestamp}.{fbclid_value} (e.g., fb.1.1612345678901.AbCdEfGhIjKlMnOp)
    • If not present as a cookie, you can construct it from the fbclid URL parameter: fb.1.{current_timestamp_ms}.{fbclid}
    • Do not hash -- send the raw value
    // Server-side: extract fbp and fbc from cookies
    function extractMetaCookies(cookieHeader) {
      const cookies = Object.fromEntries(
        cookieHeader.split(';').map(c => c.trim().split('='))
      );
      return {
        fbp: cookies['_fbp'] || null,
        fbc: cookies['_fbc'] || null,
      };
    }
    
    // If _fbc is missing but fbclid is in the URL
    function constructFbc(fbclid) {
      if (!fbclid) return null;
      return `fb.1.${Date.now()}.${fbclid}`;
    }
    

    Common Dedup Failures

    ProblemSymptomFix
    Different event_id on Pixel vs CAPIDouble-counted conversionsGenerate event_id in browser, pass to server
    event_id missing from PixelNo dedup possibleAdd { eventID: id } as 4th argument to fbq('track', ...)
    event_id missing from CAPINo dedup possibleInclude event_id in CAPI payload
    Different event_name casingEvents not matchedUse exact standard event names (e.g., Purchase, not purchase)
    CAPI event sent hours laterEvents outside dedup window (48h)Send CAPI events within minutes, not in delayed batches
    Pixel fires multiple timesMultiple Pixel events with different IDsEnsure Pixel fires only once per conversion action

    Testing and Validation

    Test Events Tool

    Meta provides a Test Events tool in Events Manager to validate your CAPI implementation before going live.

    How to use:

    1. Open Events Manager > Select your Pixel > Test Events tab
    2. Copy the Test Event Code (e.g., TEST12345)
    3. Add test_event_code to your CAPI payload:
    const payload = {
      data: [{ /* your event data */ }],
      test_event_code: 'TEST12345',  // Add this field
    };
    
    1. Send events -- they appear in the Test Events tab in real time
    2. Remove test_event_code before going to production -- test events are not processed for ad delivery

    Payload Inspection

    Use the Events Manager Overview tab to inspect received events:

    • Events Received: Total events arriving via Pixel and CAPI
    • Events Matched: Events successfully matched to a Meta user
    • Event Match Quality: Per-event EMQ score breakdown
    • Dedup Rate: Percentage of events deduplicated (indicates both paths are working)
    • Top Parameters: Which customer info parameters are being sent most frequently

    Event Match Quality Dashboard

    Navigate to Events Manager > Data Sources > Your Pixel > Event Match Quality to see:

    • Per-event EMQ scores
    • Which parameters are most frequently sent
    • Recommendations for improving match quality
    • Trend over time

    Common Error Codes and Fixes

    Error CodeMessageFix
    190Invalid OAuth access tokenRegenerate your system user token; check it has not expired
    100Invalid parameterCheck required fields: event_name, event_time, action_source, and at least one user_data field
    2804003Event timestamp too oldevent_time must be within the last 7 days (ideally within minutes)
    2804004Invalid event_timeevent_time must be a Unix timestamp in seconds (not milliseconds)
    2804001Missing user_dataInclude at least one customer information parameter
    368Temporarily blockedRate limiting; implement exponential backoff
    2804002Invalid action_sourceMust be one of: website, app, phone_call, chat, email, in_store, other
    803Permission deniedSystem user needs ads_management permission for the ad account

    Aggregated Event Measurement (AEM)

    What It Is

    Aggregated Event Measurement (AEM) is Meta's protocol for measuring web and app events from iOS 14.5+ users who have opted out of tracking via Apple's App Tracking Transparency (ATT) prompt. It was introduced in 2021 to maintain some conversion measurement capability under Apple's privacy restrictions.

    How AEM Works

    • Limited data: Meta receives delayed, aggregated conversion data for opted-out iOS users (not real-time, not user-level)
    • 8-event limit: You can configure up to 8 conversion events per domain, ranked by priority
    • Statistical modeling: Meta uses statistical methods to estimate conversions from opted-out users
    • 72-hour delay: Conversion data from opted-out users may take up to 3 days to appear in reporting
    • 1-day attribution: AEM restricts attribution to a 1-day click window for opted-out users (vs. the standard 7-day click / 1-day view default)

    8-Event Priority Ranking

    You configure up to 8 events per domain in Events Manager, ranked from highest to lowest priority. When a user performs multiple conversion actions in a single session, only the highest-priority event is reported.

    Recommended priority ranking for e-commerce:

    PriorityEventRationale
    1 (highest)PurchaseHighest-value conversion
    2SubscribeRecurring revenue signal
    3StartTrialTrial-to-paid pipeline
    4InitiateCheckoutStrong purchase intent
    5AddPaymentInfoCheckout progression
    6AddToCartMid-funnel engagement
    7CompleteRegistrationAccount creation
    8 (lowest)ViewContentTop-of-funnel awareness

    Recommended priority ranking for lead generation:

    PriorityEventRationale
    1 (highest)PurchaseClosed deal
    2LeadForm submission
    3SubmitApplicationApplication submitted
    4ScheduleDemo/appointment booked
    5CompleteRegistrationAccount created
    6ContactInitiated contact
    7ViewContentKey page viewed
    8 (lowest)SearchSite search performed

    Domain Verification

    AEM requires domain verification to configure event priority:

    1. Go to Business Settings > Brand Safety > Domains
    2. Add your domain and verify via one of:
      • DNS TXT record (recommended): Add a TXT record to your domain's DNS
      • HTML file upload: Upload a verification file to your web root
      • Meta tag: Add a meta tag to your homepage <head>
    3. Once verified, configure your 8 events in Events Manager > Aggregated Event Measurement

    Impact on Reporting

    MetricPre-iOS 14.5Post-iOS 14.5 (AEM)
    Attribution window28-day click, 7-day view7-day click, 1-day view (default)
    Reporting delayReal-timeUp to 72 hours for opted-out users
    Breakdown dimensionsFull (age, gender, placement, etc.)Limited (some breakdowns unavailable)
    Conversion countExactModeled/estimated for opted-out users
    Optimization eventsUnlimited8 per domain

    Mitigations:

    • Use CAPI to maximize matched events (higher EMQ partially offsets ATT opt-outs)
    • Prioritize the 8 events that matter most for optimization
    • Expect 15-30% underreporting on iOS conversions; use server-side data as source of truth
    • Consider broad targeting strategies that rely less on individual user tracking

    Browser Pixel + CAPI Dedup Pattern (Complete Example)

    This example shows the full end-to-end pattern for a Purchase event with proper deduplication.

    Browser Side

    <!-- Meta Pixel base code (in <head>) -->
    <script>
    !function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
    n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
    n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
    t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
    document,'script','https://connect.facebook.net/en_US/fbevents.js');
    fbq('init', 'YOUR_PIXEL_ID');
    fbq('track', 'PageView');
    </script>
    
    <!-- On the purchase confirmation page -->
    <script>
    (function() {
      // Generate a unique event_id for deduplication
      var eventId = 'purchase_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
    
      // Fire the Pixel event with eventID
      fbq('track', 'Purchase', {
        value: 149.99,
        currency: 'USD',
        content_ids: ['SKU-001', 'SKU-002'],
        content_type: 'product',
        num_items: 2,
      }, { eventID: eventId });
    
      // Send the same event data + event_id to your server for CAPI
      navigator.sendBeacon('/api/meta-capi', JSON.stringify({
        event_name: 'Purchase',
        event_id: eventId,
        source_url: window.location.href,
        custom_data: {
          value: 149.99,
          currency: 'USD',
          content_ids: ['SKU-001', 'SKU-002'],
          content_type: 'product',
          num_items: 2,
        },
      }));
    })();
    </script>
    

    Server Side

    // POST /api/meta-capi
    app.post('/api/meta-capi', async (req, res) => {
      const { event_name, event_id, source_url, custom_data } = req.body;
    
      // Extract user data from your session/database
      const user = await getAuthenticatedUser(req);
    
      // Extract Meta cookies
      const fbp = req.cookies['_fbp'];
      const fbc = req.cookies['_fbc'] || constructFbc(req.query.fbclid);
    
      await sendEvent(event_name, {
        source_url,
        customData: custom_data,
      }, {
        email: user?.email,
        phone: user?.phone,
        firstName: user?.firstName,
        lastName: user?.lastName,
        ipAddress: req.ip,
        userAgent: req.headers['user-agent'],
        fbp,
        fbc,
        externalId: user?.id,
      }, event_id);  // Same event_id as the Pixel
    
      res.status(200).json({ ok: true });
    });
    

    Next Steps

    Claude Code Skill

    This CAPI reference is also available as a free Claude Code skill -- use it directly in your terminal:

    # Install
    curl -sSL https://raw.githubusercontent.com/cognyai/claude-code-marketing-skills/main/install.sh | bash
    
    # Use
    /meta-capi                          # Full CAPI overview
    /meta-capi deduplication            # Event dedup strategy
    /meta-capi purchase event           # Purchase event parameters
    /meta-capi emq                      # Improve Event Match Quality
    

    View on GitHub →

    Resources

    Ready for similar results?
    Start with Solo at $9/mo or talk to us about Cloud.
    ❯ get startedcompare plans →