One API for Meta, GA4, TikTok and Pinterest tracking: meet PixelFlow
Here’s an uncomfortable thought: your checkout can work perfectly, your ads can be running, and your conversion tracking can still be quietly broken. No error. No crash. Just ad platforms learning from bad data (or no da
Here’s an uncomfortable thought: your checkout can work perfectly, your ads can be running, and your conversion tracking can still be quietly broken. No error. No crash. Just ad platforms learning from bad data (or no data at all) while the marketing team wonders why ROAS keeps slipping.
If you’ve ever wired up ad tracking for a SaaS, a website or an e-commerce store, you know how easy it is to end up there. Every social network has its own quirks, and those quirks add up fast. That’s why I built PixelFlow: an open-source TypeScript library that gives you one standardized interface for Meta, Google Analytics 4, TikTok and Pinterest, on both the browser and the server.
In this post I’ll walk through the problem, how PixelFlow approaches it, and how to get it running in a few minutes. If you like what you see, there’s a star button and a contributing section waiting at the end.
Why ad tracking is harder than it looks
On paper, tracking a purchase is one function call. In practice, every platform speaks its own dialect:
-
Events don’t map one-to-one. Some events exist on Meta but not on Pinterest. Some exist on Meta and Pinterest but not on TikTok. Pinterest, for example, has no native
AddPaymentInfo, so you end up inventing custom events and remembering which platform gets which. -
Same idea, different keys. One SDK wants
paymentType, another wantspayment_type, and so on for dozens of properties. - Same value, different types. On the server side, a purchase amount that Meta accepts as a number may need to be a string for Pinterest. Add payment gateways that report cents while platforms expect decimals, and you have the perfect recipe for off-by-100 revenue.
-
Different catalog shapes. GA4 wants an
itemsarray, others wantcontents, each with its own field names. - Hashing rules. Meta and TikTok expect normalized, SHA-256 hashed emails and phone numbers when you send events from your backend.
- Deduplication. Fire the same event from the browser and the server without a shared event ID and you’ll count the sale twice.
- Consent. Under GDPR and LGPD, nothing should fire unless the user has agreed.
Get any of these wrong and nothing blows up. The event just doesn’t match, doesn’t deduplicate, or never arrives, and you find out weeks later while staring at a dashboard that doesn’t add up.
Browser-only tracking has its own ceiling, too: AdBlockers and strict cookie protections like Safari’s ITP can drop events before they ever leave the page.
Meet PixelFlow
PixelFlow is an Omnichannel Tracking Hub. Your app calls one method, and PixelFlow translates the event and distributes it to every platform you enable. It’s built around three ideas:
- One interface, many platforms. You describe the event once, in a clean typed payload. PixelFlow maps the exact keys each platform requires and creates custom events where a platform has no native equivalent, so no data is lost along the way.
-
Hybrid architecture. Browser-side tracking (pixels and tags) and server-side tracking (Conversions APIs) live in the same ecosystem and share the same payload shape, with deduplication built around a shared
eventId. -
Privacy by default. Every event passes through a consent lock. If
hasConsentedisfalseor omitted, the event is dropped and a warning is logged to the console.
Here’s the difference in practice. Without a unifying layer, a single purchase looks something like this (simplified):
fbq('track', 'Purchase', { value: 150.5, currency: 'USD' }, { eventID: 'ORDER-999' });
gtag('event', 'purchase', { transaction_id: 'ORDER-999', value: 150.5, currency: 'USD', items: [/* GA4 shape */] });
ttq.track('CompletePayment', { /* TikTok shape */ });
pintrk('track', 'checkout', { /* Pinterest shape */ });
With PixelFlow, it’s one call:
import { trackPurchase } from '@jpmergulhao/pixelflow-client';
trackPurchase({
eventId: 'ORDER-999',
hasConsented: true,
platforms: ['meta', 'google', 'tiktok', 'pinterest'],
currency: 'USD',
value: 150.5,
orderId: 'ORDER-999',
contents: [{ id: 'PROD-01', name: 'Awesome Product', price: 150.5, quantity: 1 }]
});
Same payload, four platforms, fully typed.
Four packages, install only what you need
PixelFlow is a monorepo of scoped packages. They work together, but each one can be installed on its own, so your bundle only contains what your environment actually needs.
| Package | Runs on | What it does |
|---|---|---|
@jpmergulhao/pixelflow-client |
Browser | Injects the base tags and fires client-side events. Uses subpath exports for framework-specific initializers (for example, /next). |
@jpmergulhao/pixelflow-react |
Browser (React / Next.js) |
PixelFlowProvider and the usePixelFlow hook, built on dependency injection. |
@jpmergulhao/pixelflow-server |
Server | Talks directly to the Conversions APIs. Uses the native Web Crypto API, so it runs on Node.js, Next.js Edge, Vercel Functions and Cloudflare Workers. |
@jpmergulhao/pixelflow-types |
Both | The shared TypeScript contract that keeps browser and server payloads identical. |
Only need server-side events? Install just the server package. Building a full-stack Next.js app? Install the three runtime packages. You’ll rarely import the types package directly, since your editor picks up the types through the other packages.
Getting started
1. Install
npm install @jpmergulhao/pixelflow-client @jpmergulhao/pixelflow-react @jpmergulhao/pixelflow-server
2. Load the base tags (Next.js)
Drop the init component into your root layout. It uses next/script under the hood, so scripts load asynchronously without blocking rendering. Only add the IDs for the platforms you use.
// app/layout.tsx
import { PixelFlowNextInit } from '@jpmergulhao/pixelflow-client/next';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang='en-US'>
<body>
<PixelFlowNextInit config={{ metaId: 'YOUR_META_ID', googleId: 'YOUR_GA4_ID' }} />
{children}
</body>
</html>
);
}
Not on Next.js? Call initPixelFlow({ ... }) once when your app mounts. The docs include ready-made examples for vanilla React, Vue 3, Angular and plain JavaScript.
3. Wrap your app once
The provider holds the settings you’d otherwise repeat on every call: the user’s consent and the active platforms.
import { PixelFlowProvider } from '@jpmergulhao/pixelflow-react';
export default function App({ Component, pageProps }) {
return (
<PixelFlowProvider
initialConsent={true} // drive this from your cookie banner state
initialPlatforms={['meta', 'google', 'tiktok', 'pinterest']}
>
<Component {...pageProps} />
</PixelFlowProvider>
);
}
4. Fire events from any component
The hook injects hasConsented and platforms for you, so the call site stays tiny:
import { usePixelFlow } from '@jpmergulhao/pixelflow-react';
export function CheckoutButton() {
const { trackPurchase } = usePixelFlow();
const handlePaymentSuccess = () => {
trackPurchase({
eventId: 'ORDER-999',
value: 150.0,
currency: 'USD',
orderId: 'ORD-999',
contents: [{ id: 'PROD-01', name: 'Awesome Product', price: 150.0, quantity: 1 }]
});
};
return <button onClick={handlePaymentSuccess}>Complete Purchase</button>;
}
5. Confirm the sale from your backend
The browser can be closed, blocked or offline. Your payment webhook can’t. Fire the purchase from the server when the order becomes Paid:
import { PixelFlowServer } from '@jpmergulhao/pixelflow-server';
const pixelFlow = new PixelFlowServer({
meta: {
pixelId: process.env.META_PIXEL_ID!,
accessToken: process.env.META_ACCESS_TOKEN!
},
pinterest: {
advertiserId: process.env.PINTEREST_AD_ID!,
accessToken: process.env.PINTEREST_ACCESS_TOKEN!
}
// tiktok: { ... }, google: { ... }
});
const handlePaymentApproved = async (txResult, req) => {
await pixelFlow.trackPurchase(
{
eventId: txResult.transactionId,
hasConsented: txResult.customer.hasConsented,
platforms: ['meta', 'pinterest'],
currency: 'USD',
value: txResult.totalInCents / 100, // always decimal
orderId: txResult.orderId,
contents: txResult.items.map((item) => ({
id: item.productId,
name: item.name,
quantity: item.quantity,
price: item.priceInCents / 100
}))
},
{
clientIpAddress: req.headers.get('x-forwarded-for') ?? '0.0.0.0',
clientUserAgent: req.headers.get('user-agent') ?? '',
email: txResult.customer.email, // send raw, PixelFlow hashes it
phone: txResult.customer.phone
}
);
};
Notice what you don’t do here: no manual hashing, no per-platform payload reshaping, no sequential requests.
Why hybrid matters
The flow for a purchase looks like this:
- The user lands on the Thank You page and the browser fires
trackPurchasewith aneventId. - Your payment gateway confirms the payment and your webhook fires
trackPurchaseon the server with the sameeventId. - The ad platforms see two signals for one event and deduplicate them.
If an AdBlocker kills the pixel, or the user closes the tab before the Thank You page loads, the server-side event still registers the conversion. If both arrive, you still count one sale. That’s the whole point of keeping both sides in one library with one contract.
What PixelFlow handles for you
-
Consent lock.
hasConsentedis the first thing validated. No consent, no event. - Automatic SHA-256 hashing. Send raw emails and phone numbers from the server; PixelFlow hashes them with the Web Crypto API before transmission, which helps Event Match Quality on Meta and TikTok.
-
Platform formatting. Decimal values go in, and each platform gets the type and shape it expects. Properties like
num_items(required by Meta) are derived from yourcontentsarray. - Gaps filled with custom events. When a platform lacks a native event, PixelFlow creates a custom one so the data still arrives.
- Parallel routing. Server events are dispatched to all active platforms simultaneously.
-
Isomorphic types. The same
BaseEventcontract powers client and server, so contract mistakes show up in your editor instead of in your revenue report. -
Friendly typing. Fields like
paymentTypeorsocialMediause union types that autocomplete common values while still accepting custom ones.
Event coverage
The client package covers the full funnel:
-
Sales:
trackViewContent,trackSearch,trackAddToCart,trackRemoveFromCart,trackAddToWishlist,trackInitiateCheckout,trackAddPaymentInfo,trackPurchase,trackSubscribe,trackStartTrial,trackDownload,trackDonate -
Leads and lifecycle:
trackLead,trackCompleteRegistration,trackSubmitApplication,trackApplicationApproval,trackContact,trackSchedule,trackCustomizeProduct,trackFindLocation,trackLogin,trackShare,trackSocialClick
On the server, the documented events today are trackInitiateCheckout, trackPurchase, trackCompleteRegistration and trackLead: the ones that matter most for revenue and lead quality.
Where to find your credentials
| Platform | Client-side | Server-side |
|---|---|---|
| Meta | Pixel ID: Events Manager > Data Sources > Settings | Access Token: same page, Conversions API section |
| Google Analytics 4 | Measurement ID (G-XXXXX): Admin > Data Streams > Web Stream Details |
API Secret: same page, Measurement Protocol API secrets |
| Advertiser ID / Tag ID: Ads Manager > Ads > Conversions | Access Token: Conversions API section | |
| TikTok | Pixel ID: Ads Manager > Tools > Events > Web Events | Access Token: Pixel > Settings > Events API |
One heads-up once you’re live: the first events can take up to 24 hours to show up in the Ad Manager dashboards (especially Pinterest and TikTok), because of the platforms’ own processing time.
It’s open source, and I’d love your help
PixelFlow is MIT licensed and built in the open. Here’s how you can get involved:
- Star the repo so more developers can find it.
- Try it in a real project and tell me what felt rough. Issues and honest feedback are gold.
- Contribute. Ideas that would make a real difference: bringing more client events to the server package, adding support for more ad platforms, improving docs and examples for other frameworks, and expanding test coverage.
If tracking has ever cost you an afternoon (or a campaign), I hope PixelFlow gives that time back.
- GitHub: github.com/jpedromergulhao/pixelFlow
- npm: @jpmergulhao/pixelflow-client, @jpmergulhao/pixelflow-react, @jpmergulhao/pixelflow-server, @jpmergulhao/pixelflow-types
Which platform’s quirk has bitten you the hardest? Tell me in the comments, and let’s see if PixelFlow can handle it.
I’m João Pedro Mergulhão, and I build PixelFlow in the open. You can find my other projects and ways to reach me at jpmergulhao.com
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.