Home / eCommerce / META CONVERSIONS API (CAPI) — PURCHASE (SERVER-SIDE)
Duplicate Snippet

Embed Snippet on Your Site

META CONVERSIONS API (CAPI) — PURCHASE (SERVER-SIDE)

Code Preview
php
<?php
/**
 * =============================================================================
 * VOELGOED — META CONVERSIONS API (CAPI) — PURCHASE (SERVER-SIDE)
 * =============================================================================
 * - Captures fbp/fbc + IP + UA at checkout into order meta
 * - Sends Purchase via Conversions API when order is PAID (processing/completed)
 * - Works with PayFast / PayStack / Yoco webhooks (thank-you not required)
 * - Deduplicates with browser Pixel using event_id = "order_{order_id}"
 *
 * REQUIREMENTS:
 * - Set your CAPI access token (Events Manager -> Conversions API)
 * - Ensure browser Pixel Purchase uses the same eventID
 * =============================================================================
 */
/** ---------------------------------------------------------------------------
 * SMALL UTILITIES
 * ------------------------------------------------------------------------- */
function vg_meta_capi_log($message, $context = []) {
    $uploads = wp_upload_dir();
    $file = trailingslashit($uploads['basedir']) . 'vg-meta-capi.log';
    $line = '[' . gmdate('Y-m-d H:i:s') . ' UTC] ' . $message;
    if (!empty($context)) {
        $line .= ' | ' . wp_json_encode($context);
    }
    $line .= PHP_EOL;
    // Best effort logging (won't fatal)
    @file_put_contents($file, $line, FILE_APPEND);
}
function vg_meta_capi_get_ip() {
    if (class_exists('WC_Geolocation') && method_exists('WC_Geolocation', 'get_ip_address')) {
        return WC_Geolocation::get_ip_address();
    }
    return isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '';
}
function vg_meta_capi_get_ua() {
    return isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '';
}
function vg_meta_capi_hash($value, $type = 'generic') {
    if ($value === null) return '';
    $v = (string) $value;
    $v = trim($v);
    $v = strtolower($v);
    if ($type === 'phone') {
        $v = preg_replace('/\D+/', '', $v);
    } elseif ($type === 'zip') {
        $v = preg_replace('/\s+/', '', $v);
    } elseif ($type === 'country') {
        $v = preg_replace('/\s+/', '', $v);
    }
    if ($v === '') return '';
    return hash('sha256', $v);
}
/** ---------------------------------------------------------------------------
 * 1) CAPTURE CLIENT CONTEXT AT CHECKOUT (fbp/fbc/ip/ua/url)
 * ------------------------------------------------------------------------- */
add_action('woocommerce_checkout_create_order', 'vg_meta_capi_capture_checkout_context', 10, 2);
function vg_meta_capi_capture_checkout_context($order, $data) {
    if (!is_a($order, 'WC_Order')) return;
    // Capture browser IDs if available
    $fbp = isset($_COOKIE['_fbp']) ? sanitize_text_field($_COOKIE['_fbp']) : '';
    $fbc = isset($_COOKIE['_fbc']) ? sanitize_text_field($_COOKIE['_fbc']) : '';
    // Capture IP + UA at checkout time (useful when paid later via webhook)
    $ip = vg_meta_capi_get_ip();
    $ua = vg_meta_capi_get_ua();
    // Capture the URL customer was on
    $url = '';
    if (!empty($_SERVER['HTTP_HOST']) && !empty($_SERVER['REQUEST_URI'])) {
        $scheme = is_ssl() ? 'https://' : 'http://';
        $url = $scheme . sanitize_text_field($_SERVER['HTTP_HOST']) . sanitize_text_field($_SERVER['REQUEST_URI']);
    }
    if ($fbp) $order->update_meta_data('_vg_fbp', $fbp);
    if ($fbc) $order->update_meta_data('_vg_fbc', $fbc);
    if ($ip)  $order->update_meta_data('_vg_client_ip', $ip);
    if ($ua)  $order->update_meta_data('_vg_client_ua', $ua);
    if ($url) $order->update_meta_data('_vg_event_source_url', $url);
}
/** ---------------------------------------------------------------------------
 * 2) TRIGGERS WHEN PAID (gateway/webhook-safe)
 * ------------------------------------------------------------------------- */
add_action('woocommerce_payment_complete', 'vg_meta_capi_on_payment_complete', 10, 1);
function vg_meta_capi_on_payment_complete($order_id) {
    vg_meta_capi_maybe_send_purchase((int)$order_id, 'payment_complete');
}
add_action('woocommerce_order_status_changed', 'vg_meta_capi_on_status_changed', 10, 4);
function vg_meta_capi_on_status_changed($order_id, $old_status, $new_status, $order) {
    // Only send when order becomes a PAID/fulfilled state
    if (!in_array($new_status, ['processing', 'completed'], true)) {
        return;
    }
    vg_meta_capi_maybe_send_purchase((int)$order_id, 'status_changed_' . $old_status . '_to_' . $new_status);
}
/** ---------------------------------------------------------------------------
 * 3) SEND PURCHASE VIA CAPI (ONCE PER ORDER)
 * ------------------------------------------------------------------------- */
function vg_meta_capi_maybe_send_purchase($order_id, $trigger) {
    if (!$order_id || is_admin() || wp_doing_ajax()) return;
    if (empty(VG_META_PIXEL_ID) || empty(VG_META_CAPI_TOKEN) || VG_META_CAPI_TOKEN === 'PASTE_YOUR_CAPI_ACCESS_TOKEN_HERE') {
        vg_meta_capi_log('CAPI not configured (missing token).', ['order_id' => $order_id]);
        return;
    }
    $order = wc_get_order($order_id);
    if (!$order) return;
    // Only send if order is actually in a paid state
    $status = $order->get_status();
    if (!in_array($status, ['processing', 'completed'], true)) {
        return;
    }
    // Do not send twice
    if ($order->get_meta('_vg_capi_purchase_sent')) {
        return;
    }
    // Consent gate (plug your consent logic in here via filter)
    $can_track = apply_filters('vg_meta_capi_can_track', true, $order_id, $trigger);
    if (!$can_track) return;
    $event_id = 'order_' . (string) $order_id;
    // Build contents
    $content_ids = [];
    $contents = [];
    foreach ($order->get_items() as $item) {
        if (!is_a($item, 'WC_Order_Item_Product')) continue;
        $product_id   = (int) $item->get_product_id();
        $variation_id = (int) $item->get_variation_id();
        $id = $variation_id ? $variation_id : $product_id;
        $qty = (int) $item->get_quantity();
        if ($id <= 0 || $qty <= 0) continue;
        $line_total = (float) $item->get_total();
        $unit_price = $qty > 0 ? ($line_total / $qty) : 0.0;
        $content_ids[] = (string) $id;
        $contents[] = [
            'id'         => (string) $id,
            'quantity'   => $qty,
            'item_price' => round((float) $unit_price, 2),
        ];
    }
    $content_ids = array_values(array_unique($content_ids));
    // User data (hash required for these fields)
    $billing_email = $order->get_billing_email();
    $billing_phone = $order->get_billing_phone();
    $billing_fn    = $order->get_billing_first_name();
    $billing_ln    = $order->get_billing_last_name();
    $billing_city  = $order->get_billing_city();
    $billing_state = $order->get_billing_state();
    $billing_zip   = $order->get_billing_postcode();
    $billing_ctry  = $order->get_billing_country();
    $user_id = $order->get_user_id();
    // Prefer captured context from checkout (survives webhooks)
    $fbp = (string) $order->get_meta('_vg_fbp');
    $fbc = (string) $order->get_meta('_vg_fbc');
    $ip  = (string) $order->get_meta('_vg_client_ip');
    $ua  = (string) $order->get_meta('_vg_client_ua');
    $src_url = (string) $order->get_meta('_vg_event_source_url');
    if (!$ip) $ip = vg_meta_capi_get_ip();
    if (!$ua) $ua = vg_meta_capi_get_ua();
    if (!$src_url) $src_url = home_url('/');
    $user_data = [];
    $em = vg_meta_capi_hash($billing_email, 'generic');
    if ($em) $user_data['em'] = [$em];
    $ph = vg_meta_capi_hash($billing_phone, 'phone');
    if ($ph) $user_data['ph'] = [$ph];
    $fn = vg_meta_capi_hash($billing_fn, 'generic');
    if ($fn) $user_data['fn'] = [$fn];
    $ln = vg_meta_capi_hash($billing_ln, 'generic');
    if ($ln) $user_data['ln'] = [$ln];
    $ct = vg_meta_capi_hash($billing_city, 'generic');
    if ($ct) $user_data['ct'] = [$ct];
    $st = vg_meta_capi_hash($billing_state, 'generic');
    if ($st) $user_data['st'] = [$st];
    $zp = vg_meta_capi_hash($billing_zip, 'zip');
    if ($zp) $user_data['zp'] = [$zp];
    $country = vg_meta_capi_hash($billing_ctry, 'country');
    if ($country) $user_data['country'] = [$country];
    if ($user_id) {
        // external_id should be hashed
        $user_data['external_id'] = [vg_meta_capi_hash((string)$user_id, 'generic')];
    }
    // IP/UA NOT hashed
    if ($ip) $user_data['client_ip_address'] = $ip;
    if ($ua) $user_data['client_user_agent'] = $ua;
    // Browser IDs NOT hashed
    if ($fbp) $user_data['fbp'] = $fbp;
    if ($fbc) $user_data['fbc'] = $fbc;
    $event = [
        'event_name'       => 'Purchase',
        'event_time'       => time(),
        'event_id'         => $event_id,
        'action_source'    => 'website',
        'event_source_url' => $src_url,
        'user_data'        => $user_data,
        'custom_data'      => [
            'currency'     => $order->get_currency() ? $order->get_currency() : 'ZAR',
            'value'        => (float) $order->get_total(),
            'content_type' => 'product',
            'content_ids'  => $content_ids,
            'contents'     => $contents,
            'order_id'     => (string) $order_id,
        ],
    ];
    $payload = [
        'data' => [$event],
    ];
    if (!empty(VG_META_CAPI_TEST_EVENT_CODE)) {
        $payload['test_event_code'] = VG_META_CAPI_TEST_EVENT_CODE;
    }
    $url = 'https://graph.facebook.com/' . rawurlencode(VG_META_CAPI_API_VERSION) . '/'
        . rawurlencode(VG_META_PIXEL_ID) . '/events?access_token=' . rawurlencode(VG_META_CAPI_TOKEN);
    $res = wp_remote_post($url, [
        'headers' => [
            'Content-Type' => 'application/json',
        ],
        'timeout' => 12,
        'body'    => wp_json_encode($payload),
    ]);
    if (is_wp_error($res)) {
        vg_meta_capi_log('CAPI request failed (WP_Error).', [
            'order_id' => $order_id,
            'trigger'  => $trigger,
            'error'    => $res->get_error_message(),
        ]);
        return;
    }
    $code = (int) wp_remote_retrieve_response_code($res);
    $body = (string) wp_remote_retrieve_body($res);
    if ($code >= 200 && $code < 300) {
        // Mark sent
        $order->update_meta_data('_vg_capi_purchase_sent', 1);
        $order->save();
        vg_meta_capi_log('CAPI Purchase sent OK.', [
            'order_id' => $order_id,
            'trigger'  => $trigger,
            'http'     => $code,
        ]);
        return;
    }
    vg_meta_capi_log('CAPI Purchase failed (non-2xx).', [
        'order_id' => $order_id,
        'trigger'  => $trigger,
        'http'     => $code,
        'body'     => $body,
    ]);
}

Comments

Add a Comment