Here’s how I’d approach tracking Facebook pageviews and conversion events in the browser (Facebook Pixel) and also server-side (Conversions API).
Facebook Browser Pixel
The Facebook Pixel loads on every page, sending pageview requests from an app or websites’ front-end to meta for ad tracking. When I configure this on a site it involves:
- Adding the Facebook Pixel to Google Tag Manager (GTM) and loading it on all public pages
- Defining triggers inside of a website or application’s codebase that push custom events to the GTM
dataLayer, each with aneventproperty naming the event
Important to note that the events I send into GTM like this are named specific to the business so that they can be used with any ad network and not just meta. I don’t send in “CompleteRegistration”, it might be “application_complete”.
Next, we’ll track FB pixel events in GTM by adding tags that attach to our custom triggers. When a application_complete event is received/triggered in GTM, we’ll track a fbq.track('CompleteRegistration') event with the Facebook Pixel. The same process is repeated for all of the events we’ll track in FB.
Once you have that in place you effectively have a complete browser implementation of the FB pixel.
Server-side Events: Conversions API
The best way to track browser and server events in FB is by handling them with the same triggers, and it’s best to avoid polluting an apps or sites backend code with API calls to the conversions API (CAPI) for many reasons:
- If a call to FB fails, we don’t want to affect the app functionality
- We don’t want to slow down the app processing steps, especially during a conversion funnel or user onboarding process
- It’s better to separate out concerns, keeping all the analytics related code separate and isolated
- Avoids merge conflicts with core app functionality that backend teams are working on
To do this I always set up a backend API handler, something like POST /api/analytics/, that will receive pageviews and custom events from the front-end in the exact same place where we’re pushing to the dataLayer. It’s clean, simple and makes a lot of sense.
In the API handler we’ll make calls to the Facebook Conversions API, and also other analytics and advertising platforms where we have server-side events configured.
For this case though, we’ll usually have a single JS class in the frontend, typically class Analytics { }, that handles the GTM dataLayer.push({ event: '{Event Name}' }) calls and also makes POST requests to /api/analytics/.
Within this class we’ll follow this pattern to send in browser and server events that can easily be de-duplicated:
- Make a POST request to
/api/analytics/for tracking a pageview - Push custom events to the GTM dataLayer and make requests to
POST /api/analytics/ - GTM receives the dataLayer push and tracks events with the FB Pixel
- Backend API handler receives requests to
/api/analytics, and makes CAPI requests to Facebook Conversions API
With this in place we can add an event_id to each event that is sent in with browser (pixel) and server-side (CAPI) events/pageviews. This is the best way to de-duplicate and when it’s working properly you’ll sync 100% of your browser tracks to server events. Remaining events will have a server-side event only, and that represents people who’ve blocked the FB pixel with tracking protection. If you have browser events that don’t match any server-side event something is wrong/not working the way it should.
Capturing Clicks: FBC & FBP
When people click a facebook ad and visit the site without tracking protection events are tracked in the browser and server. When FB deduplicates/connects the server and browser events it will have a record of the ad click because the browser pixel captures it.
People that have tracking protection enabled will only send in server events, the browser pixel won’t be loaded so we’ll lose the record of the ad click. If we don’t capture those ad clicks there’s no record of a person clicking an ad, and it doesn’t benefit campaign optimization in any way.
The solution for this dilemma is to save the value of ?fbc={click-id} and ?fbp={click-id} when they’re found in the URL using a 1st-party cookie. I do this in the same front-end JS class where we send in server-side events to POST /api/analytics/. Before sending I check cookies with JS, and send them in the payload if they exist. The backend receives them, and makes the HTTP request to facebook CAPI with them included as fbp and fbc parameter values. With this in place we’re capturing ad clicks for people with browser protection enabled.
Best Possible Match Rate
At this point we have duduplicated server and browser events being received by Facebook and we’re connecting 100% of FB ad clicks to pageviews and events because we’re sending the stored cookie values for fbc and fbp in with our conversions API requests.
There’s one final step: improving the match rate. It’s really common for people to think of “match rate” as a match between server and client events, but that’s not what it means. Match rate is a measure of the percentage of customer data sent from your website/app that successfully links to existing user profiles in Meta’s platform. The better the match rate, the more effective your campaign optimization because Facebook is able to identify patterns and similarities between people that click ads and convert, and those that don’t.
To get the best possible Meta/Facebook match rate we need to send in as much customer information as possible with both the pixel in the browser and the conversions api on the server.
Browser Pixel
The fbq track calls support a feature called Advanced Matching, which allows you to send SHA256 hashed customer information to FB with custom events to improve conversion tracking and audience matching by associating website visitors with their Facebook profile. It increases conversion attribution and improves the effectiveness of audience targeting. To protect privacy all PII values sent in are encrypted (SHA256).
em– Unhashed lowercase or hashed SHA-256fn– Lowercase first nameln– Lowercase last nameph– Phone as numbers beginning with country codeexternal_id– User IDge– Gender as “f” or “m”db– Birthday as YYYYMMDDct– City lowercase w/o spacesst– Two-letter state or province codezp– 5-digit zip codecountry– Two-letter country code
See Facebook’s Advanced Matching Docs for more details on implementing.
Important Note
These values should be sent with fbq.track() custom event calls, but also with the fbq('init') call as well:
fbq('init', '283859598862258', {
em: '[email protected]',
fn: 'first_name',
ln: 'last_name'
...
});
Most developers don’t do this, in fact I’ve never really seen any existing implementation that does. I always do though, and the implementations I’ve set up have worked out really well for clients.
Conversions API
With the CAPI you send in the same customer information within the user_data object of the payload:
em– Lowercase email without any leading and trailing spacesph– Phone as numbers only, beginning with 1`fn– Lowercase first nameln– Lowercase last namedb– Birthday YYYYMMDDge– Gender as eitherffor female,mfor malect– Lowercase city name with no punctuation, special characters, or spacesst– State as a 2-character ANSI abbreviation code in lowercasezp– 5 digit U.S. zip codescountry– 2-letter country codes in ISO 3166-1 alpha-2external_id– Unique ID for a visitor/user, if someone is logged-in this should be their account/user IDclient_ip_address– IP Address, must be valid IPV4 or IPV6 address with no spacesclient_user_agent– User agent of the browser corresponding to the eventfbc– Meta click ID valuefbp– Meta browser ID value
See Facebook’s Conversions API Parameters Docs for more details on implementing.
Pixel Bonus for Capturing Visitors with Browser Protection
Using the conversions API in tandem with pixel tracking will effectively capture people that have browser tracking protection enabled, but there is one added bonus you can implement to capture them without the need for server-side code.
The Facebook Pixel can be loaded as an <img> tag, which is an old school method originally meant to handle situations where someone has JavaScript disabled in their browser. You’d have to be crazy to do that today, or ever really, but the same method is also an effective way to track people that have blocked the pixel. It’s similar to what GTM does in the <noscript> portion of their tracking tag:
<img src="https://www.facebook.com/tr?id=12345
&ev=ViewContent
&cd[content_name]=ABC%20Leather%20Sandal
&cd[content_category]=Shoes
&cd[content_type]=product
&cd[content_ids]=1234
&cd[value]=0.50
&cd[currency]=USD" height="1" width="1" style="display:none"/>
More details are documented by Facebook here, but as of right now this will work when browser protection is enabled sometimes. Smarter systems recognize the image and will still block it, but if you create a rewrite on your application that proxies to this <img> you’re golden, they won’t ever block that.
That would look like this:
<img src="/minilogo.png?id=12345
&ev=ViewContent
&cd[content_name]=ABC%20Leather%20Sandal
&cd[content_category]=Shoes
&cd[content_type]=product
&cd[content_ids]=1234
&cd[value]=0.50
&cd[currency]=USD" height="1" width="1" style="display:none"/>
That’s more advanced, you’d need an nginx rule to handle the specific extension as a script. But it can be done and if it is it’s super effective and is more minimal once it’s all set up.
Conclusion
Implementing a robust Facebook tracking strategy requires more than just copy-pasting a script tag. By combining the immediate feedback of the Browser Pixel with the reliability of the Server-Side Conversions API, you create a fail-safe system that preserves data integrity even against modern tracking protections.
The key to success lies in the details: meticulous deduplication using event IDs, capturing ad clicks via FBC/FBP cookies, and maximizing your match rate through Advanced Matching. While the setup requires a significant upfront engineering effort—separating concerns between the frontend and a dedicated analytics API—the payoff is substantial. You gain ownership of your data, maintain app performance, and ultimately feed Meta’s algorithms the high-quality signals needed to lower your CPA and increase ROAS.
Whether you go the full route of a custom API handler or use the creative <img> proxy workaround, the goal remains the same: ensuring every conversion counts.