Marketing on Facebook remains a widespread method for acquiring customers. Our clients have long used Facebook Pixel — a tag in the client-side Google Tag Manager (cGTM) — to track visitors and optimize campaigns. With the transition to server-side tagging, we realized the logical need to move marketing tags to the server-side Google Tag Manager (sGTM) and utilize the Facebook Conversion API.
What’s the fundamental difference between Pixel and CAPI? While Pixel runs in the user’s browser, CAPI operates purely server-side. The advantages include reducing the load on the client-side GTM container, more accurate and reliable data transmission, and better control over the data.
In server-side GTM, several tags are available for Facebook CAPI, such as those from Stape or Facebook Incubator. However, I encountered issues with them:
- Facebook Incubator: It uses an API version that is expected to be available only until May 14, 2025, and it’s uncertain whether this tag will be updated. Additionally, it has limited functionality.
- Stape: They have developed a very robust and complex tag. However, I needed more flexibility to tailor the solution to specific needs.
Thus, we saw the need to create our FB Conversion API tag.
In the following lines, I’ll share the challenges I faced, how I overcame them, and the nifty features I’ve incorporated into the tag. Perhaps it will inspire you to try something similar.
Challenges and Solutions
Mapping Event Names
The first challenge I addressed was to easily “map” event names. Facebook has a list of recommended names like “AddToCart”, “CompleteRegistration”, and so on. They correspond to specific events from Google Analytics.
But what if I want to name an event differently? Some CAPI tags don’t even offer this option. You use the correct event name or the request will not be sent.
// EVENT NAME - transform GA4 event names to FB event names, with custom event mapping
function getFBEventName(gaEventName, data) {
const ga4ToStandardEvents = {
"add_payment_info": "AddPaymentInfo",
"add_to_cart": "AddToCart",
"add_to_wishlist": "AddToWishlist",
"sign_up": "CompleteRegistration",
"contact": "Contact",
"customize_product": "CustomizeProduct",
"donate": "Donate",
"find_location": "FindLocation",
"begin_checkout": "InitiateCheckout",
"generate_lead": "Lead",
"purchase": "Purchase",
"schedule": "Schedule",
"search": "Search",
"start_trial": "StartTrial",
"submit_application": "SubmitApplication",
"subscribe": "Subscribe",
"view_item": "ViewContent",
"page_view": "PageView",
"gtm.dom": "PageView"
};
My solution allows the use of event names in the tag as needed. It works as follows:
- Standard events are mapped automatically.
- The tag interface provides a field where I can enter a custom event name for GA4 (e.g., “custom_lead”) and its equivalent to be sent to Facebook (e.g., “Lead”).
- If none of the above options are utilized, the tag sends data with the original event name from GA4.
This approach ensures maximum simplicity and flexibility in mapping event names, allowing customization according to the project’s specific needs.
Mapping User Data
The second challenge was mapping user data that we sent to server-side GTM using the User-Provided Data variable. It would be ideal if Google and Facebook used the same data structure and format. Unfortunately, that’s not the case. For example, Google requires the phone number to include the “+” prefix, while Facebook requires the number without it.
To avoid manual mapping of this data, I created a function that can be enabled in the tag. This function automatically reads user data in GA4 format from the incoming request, adjusts it according to platform requirements, and hashes it.
Additionally, I added the option to override or supplement user data directly in the tag interface. This way, I can, for example, add missing email addresses or phone numbers.
// USER DATA - map GA4 user data to FB user data, including hashing personal information
function setUserData(event, user) {
event.user_data.em = hashFunction(user.email);
event.user_data.ph = hashFunction(phoneNumberFormatting(user.phone_number));
if (user.address) {
if (user.address[0]) { user.address = user.address[0]; }
event.user_data.fn = hashFunction(user.address.first_name);
event.user_data.ln = hashFunction(user.address.last_name);
event.user_data.ct = hashFunction(user.address.city);
event.user_data.st = hashFunction(user.address.region);
event.user_data.zp = hashFunction(user.address.postal_code);
event.user_data.country = hashFunction(user.address.country);
}
return event.user_data;
}
An interesting fact — maybe you knew this, maybe not: The Facebook Pixel in client-side GTM reads user data only once, during the tag’s initialization. That is when the first Pixel tag on the page is fired.
What does this mean? Even if you have a well-configured Lead Pixel in client-side GTM that sends all user data from a form (email, phone, address), but a PageView Pixel has already fired on the page before, you’re out of luck. The user data won’t be sent. It wasn’t available during the first event on the page, which was PageView.
CAPI doesn’t suffer from this issue. Another plus point for server-side GTM.
E-commerce Data
For eCommerce websites, it’s essential to ensure the correct transmission of product data. This can be challenging because Facebook recommends using different parameters for various eCommerce events.
I modified the tag to automatically process parameters from the GA4 items object and transform them into a format compatible with CAPI based on the specific event.
For example, with the AddToCart event, different eCommerce parameters are sent compared to the Purchase event.
// ECOMMERCE - map GA4 ecommerce items to Facebook's expected format
function ga4ItemObjectMapping(event, data) {
const contentTypeValidEvents = {
'AddToCart': true,
'Search': true,
'ViewContent': true,
'AddToWishlist': true
};
const numItemsValidEvents = {
'InitiateCheckout': true,
'Purchase': true
};
// content IDs
event.custom_data.content_ids = items.map(item => item.item_id);
// contents
event.custom_data.contents = items.map(item => {
return {
'id': item.item_id || item.item_name,
'item_price': item.price,
'quantity': item.quantity,
};
});
// content_type - add_to_cart, search, view_content, 'add_to_wishlist'
if (contentTypeValidEvents[event.event_name]) {
event.custom_data.content_type = data.contentType ? 'product_group' : 'product';
event.custom_data.content_name = items[0].item_name;
event.custom_data.content_category = items[0].item_category;
}
// num_items - begin_checkout, purchase
if (numItemsValidEvents[event.event_name]) {
event.custom_data.num_items = event.custom_data.content_ids.length;
}
// other parameters
event.custom_data.currency = eventData.currency || 'EUR';
event.custom_data.order_id = eventData.transaction_id;
return event.custom_data;
}
This approach significantly minimizes manual work in data preparation (laziness, the mother of progress 😅), saving time and reducing the risk of errors. At the same time, it ensures full compatibility with Facebook’s required format and more efficient processing of events within the CAPI integration.
Custom Data
Every implementation has its specific requirements, so we added the ability to send custom parameters within events. These parameters can be easily defined in the tag interface.
This is not an extraordinary feature, but it allows overwriting or deleting any parameters the tag automatically retrieves — including user-provided ones. For example, this enables us to anonymize an IP address, adjust the order value, or send profit information instead of revenue.
Extra Features
FB Data Object Builder
One specific challenge we occasionally encounter is the limited number of parameters that can be sent in a GA4 tag. This limit is currently set at 25 parameters per event.
To avoid exceeding this limit and filling up the available parameters with data specific only to Facebook, I created a variable in the client-side GTM which I named Facebook Data Object Builder.
This variable generates an object – fb_data
, which can contain any parameters as needed. The generated object is then inserted into the GA4 tag as a string and sent to the server-side GTM.
Our CAPI tag can automatically retrieve and process these parameters, eliminating the need to create new variables in the server-side GTM. This approach saves time and preserves parameters for other data uses.
FB CAPI Monitoring in BigQuery
Lastly, I’ve implemented a bonus feature: FB CAPI Monitoring.
To gain a comprehensive overview of the data being sent, we implemented parallel requests to BigQuery. You might think, “Isn’t everything I need visible in Facebook Manager?” Yes and no.
Facebook Manager only displays data that has been successfully received, whereas we send all data to BigQuery — including those rejected by Facebook due to errors (e.g., status code 400).
This approach makes it easier to identify potential implementation issues. The collected data is then visualized in Looker Studio, where its quality and consistency can be monitored via a user-friendly dashboard.
The table and dashboard have a unique feature. For clients who send custom dimensions to Facebook, we avoid creating special tables with new columns in BigQuery by encapsulating these dimensions into a JSON object. In Looker Studio, this object can then be parsed using REGEX to extract the necessary information.
Conclusion
Our FB CAPI tag includes a few additional minor tweaks that we won’t dive into in detail here.
Instead, we’d like to emphasize that the tag is specifically optimized for processing data from websites and web applications. Facebook CAPI for mobile apps is a separate category that we may explore in the future. With the new possibility of server-side tracking for mobile apps, the motivation is significantly higher.
You can find the FB CAPI tag along with a schema for creating a monitoring table in BigQuery in my GitHub repository.
Note: this post was originally written for Dase.