GTM setup from scratch for server-side tracking
Starting with an empty Google Tag Manager? Build the data layer, web container (variables, triggers, GA4 and Meta Pixel tags) and server container (GA4 client, Conversions API Tag) with one shared Event ID.
This guide starts from an empty Google Tag Manager web container and builds everything a shop needs for server-side tracking with TrackPilot: the data layer, every variable, trigger and tag in the web container, and the server container that sends GA4 and Meta Conversions API events. Browser and server events share one Event ID, so Meta counts each action once. Plan about an hour.
Hosting the server container itself (Container Config, custom domain, loader snippet) is covered in Server-side GTM (sGTM) setup, Steps 1–5. Do those first; this page continues from there.
Before you start
- A web container (
GTM-ABC1234) and a server container in Google Tag Manager, the server container running in TrackPilot with your domain active — below we writehttps://data.yourshop.com. - The first-party loader from the TrackPilot dashboard installed on every page of your site, and the old GTM snippet removed.
- Your Meta Pixel ID (Events Manager → your dataset) and a Conversions API access token (dataset → Settings → Generate access token).
- Optional: a GA4 Measurement ID (
G-XXXXXXXXXX) if you also want Google Analytics reports. - Someone who can change your shop's code or theme, for Step 1.
How the pieces fit
- Your shop pushes an event such as
purchaseinto thedataLayer. - The web container fires two tags on that event: a Meta Pixel tag (browser event) and a GA4 Event tag, which goes to your tagging server instead of Google.
- The server container's GA4 client receives it, and the Conversions API Tag sends it to Meta (server event).
- Both carry the same
event_id, so Meta keeps one and marks the other Deduplicated.
Step 1 — Website data layer
Your shop must tell GTM what happened, in the GA4 ecommerce format. Push these four events:
| Event | When | Fields in ecommerce |
|---|---|---|
view_item | Product page opened | items (the product), value, currency |
add_to_cart | Add to cart clicked | items (product + quantity), value, currency |
add_payment_info | Payment method chosen at checkout | items (whole cart), value, currency |
purchase | Thank-you / order-received page, once per order | transaction_id, items, value (order total), currency |
Each entry in items needs item_id, item_name, price and quantity. Use the same item_id as your product catalog (SKU or product ID). Numbers must be numbers, not text with a currency sign.
Always push { ecommerce: null } right before an ecommerce event. Otherwise GTM merges the new event with the previous one and old items leak into it.
window.dataLayer = window.dataLayer || [];
// Product page
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'view_item',
ecommerce: {
currency: 'BDT',
value: 1200,
items: [{ item_id: 'SKU-123', item_name: 'Cotton Panjabi', price: 1200, quantity: 1 }]
}
});
// Thank-you page
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'ORD-10045',
value: 2450, // order total after shipping and discounts
currency: 'BDT',
items: [
{ item_id: 'SKU-123', item_name: 'Cotton Panjabi', price: 1200, quantity: 2 },
{ item_id: 'SKU-456', item_name: 'Leather Belt', price: 50, quantity: 1 }
]
},
customer_data: { // optional, improves Meta match quality
em: '[email protected]',
ph: '8801712345678', // country code + number, digits only
fn: 'Rahim'
}
});
add_to_cart and add_payment_info have the same shape as view_item. customer_data is optional; push it only where the customer has entered it (checkout, thank-you page). Send it as plain text — the Conversions API Tag hashes it before it leaves your server. On WooCommerce, Shopify and most platforms a GA4 data-layer plugin or app produces these pushes for you; check them in GTM Preview (Step 6).
Step 2 — Web container: variables
Web container → Variables → User-Defined Variables → New. Create each one below with exactly this name — the tags in Step 4 refer to them by name.
| Name | Variable type | Setting |
|---|---|---|
| DLV - ecommerce.items | Data Layer Variable | Name ecommerce.items, version 2 |
| DLV - ecommerce.items.0.item_id | Data Layer Variable | ecommerce.items.0.item_id |
| DLV - ecommerce.items.0.item_name | Data Layer Variable | ecommerce.items.0.item_name |
| DLV - ecommerce.value | Data Layer Variable | ecommerce.value |
| DLV - ecommerce.transaction_id | Data Layer Variable | ecommerce.transaction_id |
| DLV - customer_data.em | Data Layer Variable | customer_data.em |
| DLV - customer_data.ph | Data Layer Variable | customer_data.ph |
| DLV - customer_data.fn | Data Layer Variable | customer_data.fn |
| User-Provided Data | User-Provided Data | Manual configuration: Email = {{DLV - customer_data.em}}, Phone = {{DLV - customer_data.ph}}, First name = {{DLV - customer_data.fn}} |
| Const - Currency | Constant | Your shop currency, for example BDT |
| Const - Content Type | Constant | product |
| Const - GA4 Measurement ID | Constant | G-XXXXXXXXXX |
| Const - Meta Pixel ID | Constant | Your Pixel ID, for example 123456789012345 |
| Event ID | Event Id (Community Template Gallery) | No settings — see below |
| Cookie - _fbp | 1st Party Cookie | Cookie Name _fbp |
| Cookie - _fbc | 1st Party Cookie | Cookie Name _fbc (exists only after a visit from a Meta ad) |
| JS - FB Contents (String) | Custom JavaScript | Code below — for GA4 tags |
| JS - FB Contents (Array) | Custom JavaScript | Code below — for Meta Pixel tags |
| JS - FB Content IDs | Custom JavaScript | Code below — for Meta Pixel tags |
| JS - FB Value | Custom JavaScript | Code below — only if your shop has no ecommerce.value |
| JS - FB Num Items | Custom JavaScript | Code below |
Event ID: Variables → New → Variable Configuration → Discover more variable types in the Community Template Gallery → search Event Id → Add to workspace. Save it under the name Event ID. It gives every data-layer event its own ID, and every tag that fires on the same event gets the same value — that is what deduplication needs.
Custom JavaScript code (paste each into its own variable):
JS - FB Contents (String) — GA4 sends parameters as text, so the list is turned into a JSON string:
function () {
var items = {{DLV - ecommerce.items}} || [];
return JSON.stringify(items.map(function (i) {
return { id: String(i.item_id), quantity: Number(i.quantity) || 1, item_price: Number(i.price) || 0 };
}));
}
JS - FB Contents (Array) — the browser Pixel takes a real list:
function () {
var items = {{DLV - ecommerce.items}} || [];
return items.map(function (i) {
return { id: String(i.item_id), quantity: Number(i.quantity) || 1, item_price: Number(i.price) || 0 };
});
}
JS - FB Content IDs
function () {
var items = {{DLV - ecommerce.items}} || [];
return items.map(function (i) { return String(i.item_id); });
}
JS - FB Value
function () {
var items = {{DLV - ecommerce.items}} || [];
var total = 0;
for (var k = 0; k < items.length; k++) {
total += (Number(items[k].price) || 0) * (Number(items[k].quantity) || 1);
}
return total;
}
JS - FB Num Items
function () {
var items = {{DLV - ecommerce.items}} || [];
var n = 0;
for (var k = 0; k < items.length; k++) {
n += Number(items[k].quantity) || 1;
}
return n;
}
Wherever this guide says {{DLV - ecommerce.value}} you can use {{JS - FB Value}} instead if your shop does not push ecommerce.value. For purchase the real order total is better, because it includes shipping and discounts.
Step 3 — Web container: triggers
Web container → Triggers → New:
| Name | Trigger type | Setting |
|---|---|---|
| Initialization - All Pages | Initialization | All Initialization Events |
| CE - view_item | Custom Event | Event name view_item, All Custom Events |
| CE - add_to_cart | Custom Event | Event name add_to_cart |
| CE - add_payment_info | Custom Event | Event name add_payment_info |
| CE - purchase | Custom Event | Event name purchase |
The GA4 tag and the Meta tag for one action must use the same trigger. Then they fire on the same data-layer event and get the same {{Event ID}}.
Step 4 — Web container: tags
4.1 Google tag (sends GA4 to your server)
Tags → New → Google tag. Name it GA4 - Google tag.
- Tag ID: {{Const - GA4 Measurement ID}}.
- Configuration settings → add these parameters:
| Parameter | Value | Why |
|---|---|---|
server_container_url | https://data.yourshop.com | Sends every GA4 hit to your tagging server instead of Google |
first_party_collection | true | Lets the server receive the user-provided data |
x-fb-ck-fbp | {{Cookie - _fbp}} | The Conversions API Tag reads the Meta browser ID from this |
x-fb-ck-fbc | {{Cookie - _fbc}} | The Meta click ID, for ad attribution |
event_id | {{Event ID}} | Deduplicates the server PageView with the browser PageView (4.3) |
Trigger: Initialization - All Pages. This tag also sends page_view.
4.2 GA4 Event tags
Tags → New → Google Analytics: GA4 Event, one per event: GA4 - view_item, GA4 - add_to_cart, GA4 - add_payment_info, GA4 - purchase.
- Measurement ID: {{Const - GA4 Measurement ID}}.
- Event Name: the event exactly (
view_item,add_to_cart,add_payment_info,purchase). - More Settings → Ecommerce → tick Send Ecommerce data, Data source Data Layer. This sends the
itemslist andtransaction_id. - Event Parameters:
| Parameter | Value |
|---|---|
event_id | {{Event ID}} |
value | {{DLV - ecommerce.value}} |
currency | {{Const - Currency}} |
content_ids | {{DLV - ecommerce.items.0.item_id}} |
content_type | {{Const - Content Type}} |
content_name | {{DLV - ecommerce.items.0.item_name}} |
num_items | {{JS - FB Num Items}} |
contents | {{JS - FB Contents (String)}} — must be the String version |
user_data | {{User-Provided Data}} — arrives on the server as user_data.email_address, user_data.phone_number, user_data.address.first_name |
transaction_id | {{DLV - ecommerce.transaction_id}} — purchase only |
Trigger: the matching CE - … trigger. Tip: build GA4 - view_item completely, then use Copy (⋮ menu) for the other three and change only the name, event name and trigger.
4.3 Meta Pixel tags
Tags → New → Discover more tag types in the Community Template Gallery → search Meta Pixel → Add to workspace. Use this official template, not the older Facebook Pixel (facebookarchive) one, which has no Event ID field.
Create five tags. In every one: Pixel ID = {{Const - Meta Pixel ID}}, Event Name = Standard + the Meta event, and More Settings → Event ID = {{Event ID}}. Leave the Meta-enabled Conversions API opt-in off — your server container already sends the server events.
| Tag name | Meta event | Trigger |
|---|---|---|
| Meta - PageView | PageView | Initialization - All Pages |
| Meta - ViewContent | ViewContent | CE - view_item |
| Meta - AddToCart | AddToCart | CE - add_to_cart |
| Meta - AddPaymentInfo | AddPaymentInfo | CE - add_payment_info |
| Meta - Purchase | Purchase | CE - purchase |
On the four ecommerce tags add Object Properties (Property Name → Property Value):
| Property | Value |
|---|---|
content_ids | {{JS - FB Content IDs}} |
content_type | {{Const - Content Type}} |
content_name | {{DLV - ecommerce.items.0.item_name}} |
contents | {{JS - FB Contents (Array)}} — the Array version |
num_items | {{JS - FB Num Items}} |
value | {{DLV - ecommerce.value}} |
currency | {{Const - Currency}} |
4.4 Event map
The Conversions API Tag on the server renames GA4 events to Meta events by itself. The browser tag must use the matching Meta name:
| Data layer / GA4 event | Meta event | Web tags (same trigger) |
|---|---|---|
page_view (sent by the Google tag) | PageView | GA4 - Google tag + Meta - PageView |
view_item | ViewContent | GA4 - view_item + Meta - ViewContent |
add_to_cart | AddToCart | GA4 - add_to_cart + Meta - AddToCart |
add_payment_info | AddPaymentInfo | GA4 - add_payment_info + Meta - AddPaymentInfo |
purchase | Purchase | GA4 - purchase + Meta - Purchase |
Step 5 — Server container
- Server container URL: server container → Admin → Container Settings →
https://data.yourshop.com→ Save (already done if you followed sGTM setup Step 4). - Clients:
- GA4 — present in every new server container. Keep it; it receives the events from Step 4.
- Google Tag Manager: Web Container — Clients → New → this type → Allowed Container IDs =
GTM-ABC1234. Required: without it the tagging server answersgtm.jswith 400 Bad Request and your site loads no tags at all.
- Trigger: Triggers → New → Custom → Some Events →
Client NameequalsGA4. Name itGA4 client - all events. (If Client Name is not in the list, enable it under Variables → Configure.) - Conversions API Tag: Templates → Tag Templates → Search Gallery → Conversions API Tag by facebookincubator → Add. Then Tags → New → Conversions API Tag:
| Field | Value |
|---|---|
| Pixel ID | Your Meta Pixel / dataset ID |
| API Access Token | The token from Events Manager → dataset → Settings |
| Action Source | Website |
| Extend Meta Pixel cookies (fbp/fbc) | On |
| Enable Event Enhancement | On |
| Test Event Code | Only while testing (Step 6). Remove it before going live, or Meta treats every server event as a test. |
| Trigger | GA4 client - all events |
Optional — Google Analytics reports. With server_container_url set, GA4 hits stop going to Google directly. To keep your GA4 reports, add Tags → New → Google Analytics: GA4 (leave Measurement ID empty to use the one in the event) with the same GA4 client - all events trigger.
Step 6 — Publish and verify
- Publish the server container first (Submit → Publish), then the web container. The other way round, the website starts sending to a server that has no Web Container client or CAPI tag yet. The tagging server loads a new version within about a minute.
- Open your site and hard-reload with Ctrl+Shift+R — browsers keep the old
gtm.jsfor a few minutes. - Web Preview (web container → Preview): view a product, add to cart, check out. For each event,
GA4 - …andMeta - …both show as fired, and on the Variables tab {{Event ID}} has one value. - Server Preview (server container → Preview, keep that tab open, then use the site in another tab): each request is claimed by the GA4 client. In Event Data check
event_id,items,value(a number),contents(a JSON list in quotes),x-fb-ck-fbpand, at checkout,user_data. Under Tags the Conversions API Tag fired, and its outgoing request tograph.facebook.comreturned 200. - Events Manager → Test events: put the Test Event Code into the CAPI tag, publish, and repeat the actions. Each one shows a Browser row and a Server row; the server row says Deduplicated and both share one Event ID.
- Meta Pixel Helper (Chrome extension): open an event — it shows the Event ID and the
contentsthe browser sent. - Remove the Test Event Code, publish the server container again. Done.
Common pitfalls
contentsshows[object Object]on the server: a GA4 parameter that holds an array is sent as text. Use {{JS - FB Contents (String)}} in GA4 tags; keep the Array version for the Pixel.- Server events have only
valueandcurrency: theitemslist is not sent. Tick Send Ecommerce data on every GA4 Event tag. - No Event ID field on the Pixel tag: you are on the old "Facebook Pixel" (facebookarchive) template. Add the Meta Pixel template, or accept the update GTM offers, and set More Settings → Event ID.
- Changes don't show after publishing:
gtm.jsis cached — hard-reload, or wait a few minutes. - The Browser row in Test events has no Event ID: that column is often empty for browser rows even when the ID is sent. Check in Meta Pixel Helper; if the server row says Deduplicated, it works.
Troubleshooting
- No tags fire at all — the loader's
gtm.jsreturns 400: the Web Container client is missing or the server container is not published (Step 5). - GA4 tag fires but nothing reaches the server — check
server_container_urlon the Google tag, and that your domain shows active in the TrackPilot dashboard. - Server event arrives, CAPI tag doesn't fire — the trigger must be Client Name equals GA4, and the GA4 client must be the one claiming the request.
- CAPI request returns 400 — wrong Pixel ID or expired access token; the response body in Server Preview says which.
- Events counted twice — the GA4 and Meta tags of one action use different triggers, so their Event IDs differ (Step 3).
More causes and fixes: the sGTM setup troubleshooting table. How Meta matches browser and server events: Event deduplication.
Stuck? Ask for setup help — we answer in বাংলা or English.