Payment Provider SDK Callback InitiateCheckout Purchase

Track events when third-party payment widgets emit client-side events (Paddle, Gumroad, Lemon Squeezy, etc.)

When to use: Use this approach when a third-party payment provider controls the checkout UI — they embed a widget, iframe, or redirect to their hosted page where users enter payment details. Since you don't control their page, you can't use standard click tracking. Instead, you listen for JavaScript events that the provider's SDK emits when actions occur.

Try the Payment Flow

Click the button below to open a simulated third-party payment modal. Watch the Event Monitor panel to see events fire in real-time.

Order Details
Product: Pro Plan Annual
Amount: $149.99
Customer: buyer@example.com
/**
 * Payment Provider SDK Integration
 *
 * Handle client-side events from embedded payment widgets/SDKs.
 * Replace 'paymentProvider' with your SDK instance (Paddle, Gumroad, etc.)
 */

// Helper function with retry logic for PixelFlow availability
async function trackPurchaseEvent(eventData, retryCount = 0) {
  const maxRetries = 3;

  // Wait for PixelFlow to initialize
  if (
    !window.pixelFlow?.trackEvent ||
    !window.pixelFlow?.utils?.normalizeCustomerData
  ) {
    if (retryCount < maxRetries) {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      return trackPurchaseEvent(eventData, retryCount + 1);
    }
    console.error("PixelFlow not available after retries");
    return false;
  }

  try {
    // Build rich customData with all available fields
    const customData = {
      value: eventData.amount / 100,              // Monetary value (required for Purchase)
      currency: eventData.currency.toUpperCase(), // ISO 4217 code: "USD", "EUR", "GBP"
      content_type: "product",                    // "product" or "product_group"
      content_name: eventData.productName,        // Product or content name
      content_ids: eventData.items.map(item => item.sku), // Product SKUs
      num_items: eventData.items.reduce((sum, item) => sum + item.quantity, 0),
      contents: eventData.items.map(item => ({
        id: item.sku,                             // Product ID or SKU
        quantity: item.quantity,                  // Quantity purchased
        item_price: item.price / 100              // Price per item
      }))
    };

    // Normalize and prepare user data (hashing is automatic)
    const userData = await window.pixelFlow.utils.normalizeCustomerData({
      em: eventData.customer?.email,              // Email (will be normalized & hashed)
      fn: eventData.customer?.firstName,          // First name (will be normalized & hashed)
      ln: eventData.customer?.lastName,           // Last name (will be normalized & hashed)
      ph: eventData.customer?.phone               // Phone (will be normalized & hashed)
    });

    const success = await window.pixelFlow.trackEvent("Purchase", customData, userData);

    if (success) {
      console.log("Purchase event tracked successfully");
    }
    return success;
  } catch (error) {
    console.error("Error tracking purchase:", error);
    return false;
  }
}

// Listen for payment provider's completion event
paymentProvider.on("purchase.complete", async (event) => {
  try {
    await trackPurchaseEvent({
      amount: event.data?.amount,              // Amount in cents (e.g., 14999 = $149.99)
      currency: event.data?.currency,          // Currency code
      productName: event.data?.productName,
      items: event.data?.items || [],          // Array of purchased items
      customer: {
        email: event.data?.customer?.email,
        firstName: event.data?.customer?.firstName,
        lastName: event.data?.customer?.lastName,
        phone: event.data?.customer?.phone
      }
    });
  } catch (error) {
    console.error("Error in purchase.complete handler:", error);
  }
});

// Track checkout initiation when modal opens
paymentProvider.on("checkout.open", async (event) => {
  if (window.pixelFlow?.trackEvent) {
    // InitiateCheckout can include cart preview data
    await window.pixelFlow.trackEvent("InitiateCheckout", {
      value: event.data?.cartTotal / 100,
      currency: event.data?.currency || "USD",
      num_items: event.data?.itemCount || 1,
      content_type: "product"
    }, {});
  }
});

Cross-Page Data: localStorage Purchase

Save checkout data to localStorage, then send Purchase event on the thank-you page

Same-domain flows: Use localStorage when your checkout and thank-you pages are on the same domain. Save complete order and customer data before redirect, then retrieve and send the event on the confirmation page.

Step 1: Checkout Page — Save Data

This simulates saving order data before redirecting to the thank-you page.

Step 2: Thank-You Page — Send Event

After redirect, retrieve the data from localStorage and send the Purchase event. Watch the Event Monitor panel to see the event fire.

/**
 * localStorage Cross-Page Data Flow
 * Use when checkout and thank-you pages are on the same domain
 */

// ═══════════════════════════════════════════════════════════
// CHECKOUT PAGE: Save complete order data before redirect
// ═══════════════════════════════════════════════════════════

function saveCheckoutData(orderData) {
  const checkoutData = {
    // Order details
    orderId: orderData.orderId,
    value: orderData.total,                    // Total order value
    currency: orderData.currency,              // ISO 4217: "USD", "EUR", etc.
    
    // Product information for contents array
    items: orderData.items.map(item => ({
      id: item.sku,                            // Product SKU
      name: item.name,                         // Product name
      quantity: item.quantity,                 // Quantity
      item_price: item.price                   // Unit price
    })),
    
    // Customer information (will be normalized & hashed)
    customer: {
      email: orderData.customer.email,         // Required for matching
      firstName: orderData.customer.firstName,
      lastName: orderData.customer.lastName,
      phone: orderData.customer.phone
    },
    
    timestamp: Date.now()
  };

  localStorage.setItem("pixelflow_checkout_data", JSON.stringify(checkoutData));
  window.location.href = "/thank-you";
}

// ═══════════════════════════════════════════════════════════
// THANK-YOU PAGE: Read data and send Purchase event
// ═══════════════════════════════════════════════════════════

async function sendPurchaseFromStorage(retryCount = 0) {
  const maxRetries = 3;

  // Wait for PixelFlow with retry logic
  if (
    !window.pixelFlow?.trackEvent ||
    !window.pixelFlow?.utils?.normalizeCustomerData
  ) {
    if (retryCount < maxRetries) {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      return sendPurchaseFromStorage(retryCount + 1);
    }
    console.error("PixelFlow not available");
    return false;
  }

  const stored = localStorage.getItem("pixelflow_checkout_data");
  if (!stored) {
    console.warn("No checkout data found");
    return false;
  }

  try {
    const data = JSON.parse(stored);

    // Build comprehensive customData
    const customData = {
      value: data.value,                       // Total monetary value (required)
      currency: data.currency,                 // ISO 4217 currency code
      content_type: "product",                 // "product" or "product_group"
      content_ids: data.items.map(item => item.id),  // Array of SKUs
      num_items: data.items.reduce((sum, item) => sum + item.quantity, 0),
      contents: data.items.map(item => ({
        id: item.id,                           // Product ID/SKU
        quantity: item.quantity,               // Quantity
        item_price: item.item_price            // Unit price
      }))
    };

    // Normalize user data (hashing handled automatically)
    const userData = await window.pixelFlow.utils.normalizeCustomerData({
      em: data.customer.email,                 // Email → normalized → hashed
      fn: data.customer.firstName,             // First name → normalized → hashed
      ln: data.customer.lastName,              // Last name → normalized → hashed
      ph: data.customer.phone                  // Phone → normalized → hashed
    });

    const success = await window.pixelFlow.trackEvent("Purchase", customData, userData);

    if (success) {
      // Clear localStorage to prevent duplicate events
      localStorage.removeItem("pixelflow_checkout_data");
      console.log("Purchase tracked successfully");
    }

    return success;
  } catch (error) {
    console.error("Error sending purchase:", error);
    return false;
  }
}

// Auto-run when thank-you page loads
sendPurchaseFromStorage();

Cross-Page Data: API Fetch Purchase

Fetch order data from your backend API on the thank-you page

Cross-domain or server-stored data: Use this approach when order data is stored on your server (not in localStorage), or when the checkout happens on a different domain. Pass the order ID in the URL, then fetch complete order details from your API to send the event.

Thank-You Page — Fetch & Send

The order ID is passed via URL. Your page fetches the complete order data from your backend API. Watch the Event Monitor to see the Purchase event fire.

URL: /thank-you?order_id=ORD-2024-002
/**
 * API Fetch Cross-Page Data Flow
 * Use for cross-domain flows or when order data is stored server-side
 */

async function sendPurchaseFromAPI(retryCount = 0) {
  const maxRetries = 3;

  // Wait for PixelFlow with retry logic
  if (
    !window.pixelFlow?.trackEvent ||
    !window.pixelFlow?.utils?.normalizeCustomerData
  ) {
    if (retryCount < maxRetries) {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      return sendPurchaseFromAPI(retryCount + 1);
    }
    console.error("PixelFlow not available");
    return false;
  }

  // Get order ID from URL parameters
  const urlParams = new URLSearchParams(window.location.search);
  const orderId = urlParams.get("order_id");

  if (!orderId) {
    console.error("No order_id in URL");
    return false;
  }

  try {
    // Fetch complete order details from your backend
    const response = await fetch(`/api/orders/${orderId}`);
    if (!response.ok) throw new Error("Order not found");

    const order = await response.json();

    // Build comprehensive customData with all fields
    const customData = {
      value: order.total,                      // Total order value (required)
      currency: order.currency,                // ISO 4217: "USD", "EUR", "GBP"
      content_type: "product",                 // "product" or "product_group"
      content_name: order.items[0]?.name,      // Primary product name
      content_ids: order.items.map(item => item.sku),  // All product SKUs
      num_items: order.items.reduce((sum, item) => sum + item.quantity, 0),
      contents: order.items.map(item => ({
        id: item.sku,                          // Product ID or SKU
        quantity: item.quantity,               // Quantity purchased
        item_price: item.price                 // Unit price
      }))
    };

    // Normalize user data - PixelFlow handles hashing automatically
    const userData = await window.pixelFlow.utils.normalizeCustomerData({
      em: order.customer.email,
      fn: order.customer.firstName,
      ln: order.customer.lastName,
      ph: order.customer.phone
    });

    const success = await window.pixelFlow.trackEvent("Purchase", customData, userData);
    console.log(success ? "Purchase tracked" : "Tracking failed");
    return success;
  } catch (error) {
    console.error("Error:", error);
    return false;
  }
}

// Auto-run when thank-you page loads
sendPurchaseFromAPI();

Conditional Lead Tracking Lead

Send Lead event only after backend validation confirms lead quality

Important note: The Lead event is not sent on button click or form submission here. Instead, the backend first validates the lead (email deliverability, spam detection, business domain verification, etc.) and returns a response. Only after receiving a successful validation response does the client send the Lead event to PixelFlow.

Lead Form with Backend Validation

The form submits to your backend for validation. Only when the backend returns a success response will the Lead event be sent to PixelFlow. Watch the Event Monitor to see the event fire.

Step 1: Submit form → Step 2: Backend validates → Step 3: If valid, send event
/**
 * Conditional Lead Tracking
 * Only send Lead event after backend validation passes
 * 
 * Use case: Validate email deliverability, check for spam, 
 * verify business domain, etc. before tracking as a qualified lead
 */

async function submitLeadForm(formData) {
  const statusEl = document.getElementById("validation-status");
  statusEl.textContent = "Validating...";

  /* Expected formData structure:
  {
    email: "alex@techcorp.com",
    firstName: "Alex",
    lastName: "Johnson",
    phone: "+1 555 987 6543",
    company: "TechCorp Inc",
    estimatedValue: 5000,        // Estimated deal value
    leadSource: "Website Demo Request"
  }
  */

  try {
    // Step 1: Validate with your backend (email verification, spam check, etc.)
    const validation = await fetch("/api/validate-lead", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        email: formData.email,
        company: formData.company
      })
    });

    const result = await validation.json();

    if (!result.valid) {
      statusEl.textContent = `Validation failed: ${result.reason}`;
      return false;
    }

    statusEl.textContent = "Validation passed, tracking lead...";

    // Step 2: Track Lead event only after validation passes
    return await trackValidatedLead(formData);
  } catch (error) {
    statusEl.textContent = `Error: ${error.message}`;
    return false;
  }
}

async function trackValidatedLead(formData, retryCount = 0) {
  const maxRetries = 3;

  if (
    !window.pixelFlow?.trackEvent ||
    !window.pixelFlow?.utils?.normalizeCustomerData
  ) {
    if (retryCount < maxRetries) {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      return trackValidatedLead(formData, retryCount + 1);
    }
    return false;
  }

  try {
    // Build customData for Lead event
    const customData = {
      value: formData.estimatedValue,          // Estimated lead/deal value
      currency: "USD",                         // ISO 4217 currency code
      content_name: formData.leadSource,       // Lead source or form name
      content_category: formData.company       // Can use for company/industry
    };

    // Normalize user data - all PII is hashed automatically
    const userData = await window.pixelFlow.utils.normalizeCustomerData({
      em: formData.email,                      // Email → normalized → SHA256 hashed
      fn: formData.firstName,                  // First name → normalized → hashed
      ln: formData.lastName,                   // Last name → normalized → hashed
      ph: formData.phone                       // Phone → digits only → hashed
    });

    const success = await window.pixelFlow.trackEvent("Lead", customData, userData);

    document.getElementById("validation-status").textContent = success
      ? "Lead tracked successfully!"
      : "Tracking failed";

    return success;
  } catch (error) {
    console.error("Tracking error:", error);
    return false;
  }
}

API Quick Reference

Core Functions

Method Returns Description
trackEvent(eventName, customData, userData) Promise<boolean> Send an event to Meta both Client Side and Server Side via Conversions API
utils.normalizeCustomerData(userData) Promise<UserData> Normalize user data (hashing is automatic)

customData Properties

Property Type Description
value number Monetary value (required for Purchase)
currency string ISO 4217 code: "USD", "EUR", "GBP"
content_type string "product" or "product_group"
content_name string Product or content name
content_ids string[] Array of product SKUs
contents array [{ id, quantity, item_price }]
num_items number Total quantity of items

userData Properties

Property Description Normalization
em Email address lowercase, trimmed
fn First name lowercase, trimmed
ln Last name lowercase, trimmed
ph Phone number digits only
View Full API Documentation
PaymentProvider
This is a third-party payment widget. Your website cannot access or control this interface — it's hosted by the payment provider.
Pro Plan Annual
$149.99