Marketing pixels
How Meta and TikTok events fire from themes — automatic events, 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) and GA4 at
once. Theme code never talks to window.fbq / window.ttq directly — it calls the injected
helpers so all platforms stay aligned.
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, 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) | Skip fake / security-flagged orders |
FeeefPixels wraps EventManager with the store-level guards (pixel ids present, client
mode allowed, order suppression) already applied — 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") | Lead or Purchase (dynamic) | FeeefPixels.fireConversion({ …, phase: "draft" }) |
Order submitted (status: "pending") | Lead or Purchase (dynamic) | 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, generate_lead, purchase, begin_checkout)
fire from the same calls — no extra work.
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. Each phase fires one conversion whose event name is resolved dynamically (next section):
// 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 suppresses the pending-phase one.
Dynamic objectives
Which event a conversion actually sends 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
events; "server" means the backend sends conversions via the platform APIs instead. The
FeeefPixels guards skip client events for a platform configured server-only — when all
configured platforms are server-only, no client event fires at all. This is already built
into shouldFireClientPixels(store), which every fire* helper checks.
Suppression and dedupe
- Fake orders — orders flagged by the anti-abuse layer must not fire conversions:
every conversion path checks
shouldSuppressOrderPixelEvents(order). - Purchase dedupe — Meta events carry
eventID= order id, and fired order ids are remembered inlocalStorage, so a re-render or back-navigation cannot double-count a purchase. - 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 - No raw
window.fbq/window.ttqcalls - Fake-order suppression respected (
shouldSuppressOrderPixelEvents)
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 Lead (or configured draft objective) fires; submit and confirm the pending conversion, with no duplicate Purchase.