.dev
Storefront templates

Marketing pixels

How pixel events fire from themes — funnel events in the browser, order conversions from the server, GA4, the FeeefPixels helper, dynamic Lead/Purchase objectives, and testing.

Merchants configure pixel ids on the store (store.metaPixelIds, store.tiktokPixelIds, plus Google Analytics/GTM and Microsoft Clarity ids). Lithium loads the scripts and ships an event layer that fans one logical event out to Meta (fbq), TikTok (ttq), Snap (snaptr) and GA4 at once. Theme code never talks to window.fbq / window.ttq / window.snaptr directly — it calls the injected helpers so all platforms stay aligned.

Order conversions: server first, browser where the server can't. No store loses a purchase event. For each platform the store config decides who sends the order Purchase / Lead:

Pixel configWho sends the order conversion
Mode auto / both / server and a server key (Meta access key, TikTok / Snap access token; the public store config marks the pixel capi: true)The feeef API (Conversions API / Events API), from its own order record. The browser sends nothing for that platform
Mode client, or no server keyThe browser: FeeefPixels.fireConversion fires that platform's Purchase / Lead (TikTok PlaceAnOrder, Snap PURCHASE / START_CHECKOUT, or the store / product objective) on every successful response

The decision depends only on the store config, never on the order response. Browser event ids match the server's (Meta <id>-<status>, TikTok and Snap sha256 of the id), so the platforms deduplicate if both ever send. GA4 purchase / generate_lead always comes from the browser, and the funnel events (PageView, ViewContent, AddToCart, InitiateCheckout, …) go to every platform from the browser. A pixel in server mode without a key can't send order conversions at all (its browser script isn't loaded): give it a key, or use auto / client.

How the scripts load

Every storefront route (home, products, product, checkout, thanks, contact, landing, embed, legal pages) renders a PixelsWrapper that injects the pixel scripts with Next.js lazyOnload strategy — tracking never blocks first paint or LCP. When a store has multiple Meta pixel ids, events fire per id via fbq('trackSingle', id, …).

Loading the scripts also fires the baseline PageView (Meta) / ttq.page() (TikTok) automatically.

Automatic vs manual

SurfaceWho fires events
Script loadAutomatic — PageView on every tracked route
Built-in order form (product_order_form, checkout)Automatic — ViewContent on mount, AddToCart, identify, GA4 draft/pending conversions with dedupe
Custom order forms / custom PDP componentsYour theme code — via FeeefPixels, matching the table below

If your theme replaces the native order form with custom JSX, the pixel work moves to you. The checklist at the bottom is what marketplace review expects.

Scope API

Injected into custom components (no imports):

GlobalUse
FeeefPixelsPreferred — fireViewContent, fireAddToCart, fireConversion, fireIdentify, plus the lower-level helpers re-exported
EventManagerLow-level — viewContent, addToCart, lead, purchase, initiateCheckout, identify, search
shouldSuppressOrderPixelEvents(order)Legacy. Don't use it for pixels, cart clear or the redirect: every success is handled the same

FeeefPixels wraps EventManager with the store-level guards (pixel ids present, client mode allowed) already applied for funnel events — use it unless you need something exotic.

Events reference

Match the moments Lithium's native form uses:

MomentEventCall
PDP mountViewContentFeeefPixels.fireViewContent({ store, product, currency, value })
Add to cartAddToCartFeeefPixels.fireAddToCart({ store, product, quantity, value }) after cart.add
Draft order created (status: "draft")GA4 generate_lead or purchase (dynamic); Meta / TikTok / Snap from the serverFeeefPixels.fireConversion({ …, phase: "draft" })
Order submitted (status: "pending")GA4 generate_lead or purchase (dynamic); Meta / TikTok / Snap from the serverFeeefPixels.fireConversion({ …, phase: "pending" })
Before the pending conversionAdvanced matchingFeeefPixels.fireIdentify({ store, order, phone })
Checkout start (optional)InitiateCheckoutEventManager.initiateCheckout(...)

GA4 equivalents (view_item, add_to_cart, begin_checkout) fire from the same funnel calls. For orders, fireConversion sends only GA4 purchase (transaction_id = the id in the order response) or generate_lead.

ViewContent and AddToCart

// PDP mount
React.useEffect(() => {
  if (!product) return;
  FeeefPixels.fireViewContent({
    store,
    product,
    currency: currencyCode,       // ISO code, e.g. "DZD"
    value: product.price,
  });
}, [product?.id]);

// After a successful add-to-cart
FeeefPixels.fireAddToCart({ store, product, quantity: qty, value: qty * price });

fireViewContent is deduplicated internally (a 2-second window per product id), so a re-render will not double-fire.

Conversions — draft and pending

Feeef COD forms create a draft order as the shopper fills the form, then a pending order on submit. Call fireConversion for every successful response, the same way every time — don't branch on the response. Each phase sends one GA4 event whose name is resolved dynamically (next section); Meta, TikTok and Snap get theirs from the server:

// Draft phase (form partially filled, draft order returned by the API)
FeeefPixels.fireConversion({
  store, product, order, phase: "draft", currency, value,
});

// Pending phase (real submit) — dedupe Purchase if the draft already sent one
FeeefPixels.fireIdentify({ store, order, phone: order.customerPhone });
const res = FeeefPixels.fireConversion({
  store, product, order, phase: "pending", currency, value,
  purchaseAlreadySent: purchaseEventSentRef.current,
});
purchaseEventSentRef.current = res.purchaseSent;

fireConversion returns { fired: "lead" | "purchase" | "none", purchaseSent } — keep purchaseSent in a ref so a draft-phase Purchase is not sent again on pending.

Dynamic objectives

Which event a conversion sends (in GA4 from the browser, and on the server for Meta, TikTok and Snap) is merchant-configurable per phase:

  • Draft phase reads draftObjective — default Lead.
  • Pending phase reads objective — default Purchase.

The value is resolved through a cascade (first non-empty wins):

  1. Product publicIntegrationsData.metaPixelData (when that block is enabled)
  2. Product publicIntegrationsData.tiktokPixelData
  3. Store publicIntegrations.metaPixel
  4. Store publicIntegrations.tiktokPixel
  5. Phase default (Lead for draft, Purchase for pending)

Accepted values are Lead, Purchase, or none (case-insensitive; none disables the phase). FeeefPixels.fireConversion runs this cascade for you — never hardcode Lead or Purchase in theme code.

Client vs server mode

Each platform's store integration has a mode: null / "client" / "both" fire browser funnel events; "server" means the backend sends them via the platform APIs instead. The FeeefPixels guards skip client funnel events for a platform configured server-only — when all configured platforms are server-only, no client funnel event fires at all (shouldFireClientPixels(store)). Order conversions for Meta, TikTok and Snap come from the server in every mode; the GA4 order event does not depend on mode.

Dedupe

  • Order conversions — Meta, TikTok and Snap Purchase / Lead are sent once by the server from the order record, so the browser cannot double-count them. GA4 dedupes purchase by transaction_id.
  • Draft vs pending — pass purchaseAlreadySent so a draft-phase Purchase is not sent again on pending.
  • ViewContent dedupe — 2-second in-memory window per content_ids set.

Checklist for custom COD forms

  • ViewContent on product mount
  • AddToCart on the add-to-cart CTA
  • Draft conversion via FeeefPixels.fireConversion(..., phase: "draft")
  • Pending identify + conversion (phase: "pending") with Purchase dedupe
  • fireConversion on every successful order response (no browser Meta / TikTok / Snap Purchase or Lead)
  • No raw window.fbq / window.ttq / window.snaptr calls

Testing pixels

In the browser — run the shop in a non-production build and watch the console: EventManager logs every fired event ([EventManager] Fired GA4 event…, identify payloads with masked PII) in debug mode. Meta's Pixel Helper and TikTok's Pixel Helper browser extensions confirm the script picked events up, and Meta Events Manager's Test events tab shows them arriving live.

Server-side test events — the API exposes authenticated helpers that send a test event directly to the platform (useful for verifying ids/tokens without a storefront visit):

POST /api/v1/actions/sendMetaPixelTestEvent     { id, key, code? }
POST /api/v1/actions/sendTiktokPixelTestEvent   { id, accessToken, testCode? }

code / testCode is the platform's test-event code, so the event lands in the Test events view instead of production data. See API conventions for auth.

Draft flow — fill a COD form partially on a preview shop and confirm exactly one GA4 generate_lead (or configured draft objective) fires; submit and confirm the GA4 pending conversion, with no duplicate purchase. Meta / TikTok / Snap order events show up from the server (Events Manager), not from the browser.

On this page