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 config | Who 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 key | The 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
| Surface | Who fires events |
|---|---|
| Script load | Automatic — 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 components | Your 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):
| Global | Use |
|---|---|
FeeefPixels | Preferred — fireViewContent, fireAddToCart, fireConversion, fireIdentify, plus the lower-level helpers re-exported |
EventManager | Low-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:
| Moment | Event | Call |
|---|---|---|
| PDP mount | ViewContent | FeeefPixels.fireViewContent({ store, product, currency, value }) |
| Add to cart | AddToCart | FeeefPixels.fireAddToCart({ store, product, quantity, value }) after cart.add |
Draft order created (status: "draft") | GA4 generate_lead or purchase (dynamic); Meta / TikTok / Snap from the server | FeeefPixels.fireConversion({ …, phase: "draft" }) |
Order submitted (status: "pending") | GA4 generate_lead or purchase (dynamic); Meta / TikTok / Snap from the server | FeeefPixels.fireConversion({ …, phase: "pending" }) |
| Before the pending conversion | Advanced matching | FeeefPixels.fireIdentify({ store, order, phone }) |
| Checkout start (optional) | InitiateCheckout | EventManager.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):
- Product
publicIntegrationsData.metaPixelData(when that block is enabled) - Product
publicIntegrationsData.tiktokPixelData - Store
publicIntegrations.metaPixel - Store
publicIntegrations.tiktokPixel - 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
purchasebytransaction_id. - Draft vs pending — pass
purchaseAlreadySentso a draft-phase Purchase is not sent again on pending. - ViewContent dedupe — 2-second in-memory window per
content_idsset.
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 -
fireConversionon every successful order response (no browser Meta / TikTok / Snap Purchase or Lead) - No raw
window.fbq/window.ttq/window.snaptrcalls
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.