GTM 11 min read

X's Google Tag Manager Integration: Complete Setup Guide for Conversion Tracking Without the Pixel

X just shipped an official Google Tag Manager template, meaning you can finally deploy X (Twitter) conversion tracking without hand-rolling the pixel or maintaining custom HTML tags. Most implementati

A
Ashwani Bhasin
·

X just shipped an official Google Tag Manager template, meaning you can finally deploy X (Twitter) conversion tracking without hand-rolling the pixel or maintaining custom HTML tags. Most implementation guides online still tell you to paste JavaScript into GTM — that approach is now obsolete, and if you’re still running the legacy twq('event', ...) snippets in Custom HTML tags, you’re doing extra work for worse data.

I’ve spent the last few weeks migrating client accounts from the old custom HTML pattern to X’s official template. Here’s the walkthrough I wish existed when I started: what the template actually does under the hood, how to configure base and event tags properly, how to wire up enhanced conversions with hashed user data, and where the whole thing quietly breaks if you’re not paying attention.

Why the Official Template Changes the Game

The old way of tracking X conversions in GTM looked like this: paste the base pixel into a Custom HTML tag, fire it on all pages, then create a second Custom HTML tag for each conversion event with something like twq('event', 'tw-xxxxx-xxxxx', {...}). It worked, but it had four problems I kept hitting on client accounts:

  1. No native Consent Mode support. You had to build your own consent gates using triggers, which meant marketing consent updates fired inconsistently.
  2. User-provided data was a mess. Passing hashed emails required manually SHA-256 hashing in a Custom JavaScript variable, and half the implementations I audited had the wrong hashing format.
  3. No sandboxed execution. Custom HTML tags run with full access to the page, so security-conscious clients (finance, healthcare) would flag them in reviews.
  4. Version drift. X updates the pixel JS. Custom HTML tags don’t. Old implementations quietly break when the endpoint schema shifts.

The official template solves all four. It’s a sandboxed template written to GTM’s template API, which means it uses injectScript, sendPixel, and setInWindow calls that GTM controls. You configure it through a UI, not by writing JavaScript. And it plugs into Consent Mode v2 natively.

Here’s a quick comparison of the two approaches side by side:

FeatureCustom HTML (old)Official X Template
Setup time (first pixel)20–30 min5–10 min
Consent Mode v2 integrationManual triggersBuilt-in
User-provided data hashingManual SHA-256 in Custom JSAutomatic
Sandboxed executionNoYes
Enhanced conversionsRequires custom codeNative config
Template versioningFrozen at installAuto-updates via Community Gallery
Server-side compatibleCustom logic requiredWorks with sGTM forwarding

If you’re auditing an account and you see <script>!function(e,t,n,s,u,a){...twq... sitting in a Custom HTML tag, that’s your first cleanup target.

Installing the X Base Tag in GTM

Head to your GTM container, go to Tags → New → Tag Configuration, then click Discover more tag types in the Community Template Gallery. Search for “X” or “Twitter”. You’re looking for the template published by X Corp (verified badge, not a community fork — check the publisher). Install it.

Once installed, the template shows up in the standard tag picker. Create a new tag using it and you’ll see three tag types in the dropdown:

  • Base Tag — loads the pixel library, fires a PageView
  • Event Tag — fires a specific conversion event
  • Enhanced Conversions Tag — fires with user-provided data

Start with the base tag. You need your Pixel ID, which you get from X Ads Manager under Tools → Events Manager → Pixels. It looks like o1abc (a short alphanumeric string, usually 5 characters).

Configure the base tag:

  • Pixel ID: o1abc (yours)
  • Consent Settings: Require additional consent for tag to fire → ad_storage, ad_user_data, ad_personalization
  • Trigger: All Pages (or Consent Initialization if you’re running Consent Mode properly)

That’s it for the base. Don’t add event data here — the base tag should only handle the pixel load and PageView. I’ve seen implementations try to cram purchase events into the base tag firing on all pages. That double-fires everything.

Setting Up Event Tags: Purchase, Lead, Sign-Up, Add-to-Cart

X supports a defined set of standard events. The ones you’ll actually use:

Event NameWhen to FireRequired Params
PurchaseOrder confirmation pagevalue, currency, conversion_id
AddToCartAdd-to-cart button clickvalue, currency, content_ids
LeadForm submission (contact, demo request)value, currency
SignUpAccount creation confirmationnone required, but pass content_name
ViewContentProduct detail page viewcontent_ids, content_type

For each event, create a new tag using the X template, select Event Tag, and reference the same Pixel ID as your base tag. Then map dataLayer variables into the event parameters.

Purchase event configuration

Assuming your dataLayer push on the order confirmation page looks like this (this is a standard GA4-compatible structure I use on Shopify and headless builds — see our Shopify service for how we wire this into checkout):

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  event: 'purchase',
  ecommerce: {
    transaction_id: 'ORDER-10234',
    value: 89.50,
    currency: 'GBP',
    items: [
      { item_id: 'SKU-001', item_name: 'Blue Shirt', price: 45.00, quantity: 1 },
      { item_id: 'SKU-002', item_name: 'Grey Cap',   price: 22.25, quantity: 2 }
    ]
  },
  user_data: {
    email: 'customer@example.com',
    phone: '+447700900123'
  }
});

You need GTM dataLayer variables for each field you’re passing to X. Create these under Variables → User-Defined Variables:

  • DLV - transaction_idecommerce.transaction_id
  • DLV - valueecommerce.value
  • DLV - currencyecommerce.currency
  • DLV - itemsecommerce.items
  • DLV - user_emailuser_data.email
  • DLV - user_phoneuser_data.phone

Now configure your X Purchase Event Tag:

  • Event ID: paste from X Ads Manager (each event has its own ID, like tw-o1abc-o9xyz)
  • Conversion ID: {{DLV - transaction_id}} (this deduplicates events between browser and server, don’t skip it)
  • Value: {{DLV - value}}
  • Currency: {{DLV - currency}}
  • Contents (repeated field): map content_id, content_name, num_items, content_price from your items array

For the contents array mapping, X’s template expects a specific structure. If you’re passing a GA4-style items array, you’ll need a Custom JavaScript variable to reshape it:

function() {
  var items = {{DLV - items}};
  if (!items || !Array.isArray(items)) return [];
  return items.map(function(i) {
    return {
      content_id: i.item_id,
      content_name: i.item_name,
      content_price: i.price,
      num_items: i.quantity || 1,
      content_type: 'product'
    };
  });
}

Reference this variable in the Contents field of your event tag. This is one of the few places you still need JavaScript, and it’s fine — it’s a variable, not an inline HTML tag, so it stays sandboxed.

Trigger: Custom Event = purchase.

AddToCart, Lead, SignUp

Same pattern. Create an Event Tag for each, reference the correct Event ID from X Ads Manager, pass whatever parameters make sense. For Lead, you’ll usually pass a static value (like the estimated lead value: £20 for a demo request, £5 for a newsletter signup — whatever your model says). For SignUp, at minimum pass content_name = ‘account_creation’ so you can differentiate signup sources later.

User-Provided Data and Enhanced Conversions

This is where the new template earns its keep. Enhanced conversions (X calls this “user-provided data” internally) match conversions to X users via hashed identifiers when third-party cookies are blocked or when iOS ATT breaks browser attribution. Without it, expect 20–40% attribution loss on iOS traffic depending on your audience.

In the event tag configuration, expand User Data. You’ll see fields for:

  • Email
  • Phone number
  • First name
  • Last name
  • External ID
  • City / State / Country / Zip

Map your dataLayer values directly into these fields. The template hashes them client-side using SHA-256 before sending. Do not pre-hash them yourself — the template detects already-hashed values and skips re-hashing, but the format requirements (lowercase, trimmed for email; E.164 with no plus sign for phone) trip people up. Let the template do it.

Phone number gotcha: X expects E.164 format (country code + number, no formatting). If your dataLayer has 07700 900123, that won’t match. Normalise at the source, or use a lookup variable to prepend 44 and strip formatting before passing it in.

I like to build a small normalisation variable per field:

function() {
  var phone = {{DLV - user_phone}};
  if (!phone) return undefined;
  return phone.toString()
    .replace(/[^0-9]/g, '')
    .replace(/^0/, '44'); // UK default; adjust per market
}

External ID is worth setting if you have a stable user ID (customer ID, hashed logged-in user identifier). Match rates for accounts I’ve migrated jump another 5–8% when external ID is included alongside email and phone.

The template respects Consent Mode v2 signals natively. In the base tag and every event tag, expand Advanced Settings → Consent Settings and require the following consent types:

  • ad_storage
  • ad_user_data
  • ad_personalization

If any of these are denied, the tag won’t fire, and it won’t send redacted pings either — the entire tag is blocked. This is the correct behaviour for X since X doesn’t currently support a consent-denied signal ping the way Google Ads does.

For CMPs like Cookiebot, OneTrust, or Cookieyes, make sure your CMP is configured to update Consent Mode signals when the user accepts marketing cookies. If you’re wiring this up from scratch, our GTM service team handles the full consent architecture end-to-end.

One thing worth watching: if you set the base tag to fire on Consent Initialization - All Pages, it will fire before consent is granted for users who haven’t interacted with the banner yet. Combined with the consent gating above, that’s fine — the tag evaluates consent state at fire time, not trigger time. But if you’re seeing zero PageViews in X Ads Manager, check that consent is actually being updated by your CMP, not just set to denied at pageload and never updated.

Validating in X Ads Manager and GTM Preview

Two layers of validation. Do both.

GTM Preview mode: Enter Preview, load your site, walk through a test purchase. In the Tag Assistant panel, look for the X tags firing. Click into each fired tag and check the Properties panel — you should see the pixel ID, event ID, all your mapped parameters with actual values, and (importantly) the user data fields showing hashed values (64-character hex strings). If you see raw emails there, something’s wrong with the template config.

X Ads Manager Events Manager: Go to Events Manager → your pixel → Test Events. There’s a real-time event feed. Fire your test events and they should appear within 30 seconds. Each event shows:

  • Event name
  • Timestamp
  • Match quality score (Low / Medium / High / Excellent)
  • Which user data fields were received

Aim for Match Quality of Medium or higher on Purchase events. If you’re seeing Low, you’re probably only passing email — add phone and external ID.

The X browser extension (Twitter Pixel Helper, still branded that way in the Chrome store) also works and shows the same firing data at the DOM level.

Server-Side: Forwarding Events via sGTM

If you’re running server-side GTM, you can forward events to X’s Conversions API instead of (or alongside) the browser pixel. This is where you get the biggest attribution recovery for iOS and privacy-restricted traffic.

At the time of writing, X has an official server-side template in the sGTM Community Gallery. Install it into your server container, then:

  1. In your web container, add a GA4 client-side tag or a custom event tag that forwards the purchase event (or any conversion event) to your sGTM endpoint.
  2. In your server container, create a new tag using the X server template.
  3. Configure it with your Pixel ID and API access token (generate this in X Ads Manager under Events Manager → API Access).
  4. Map event parameters and user data from the incoming event.
  5. Use the same conversion_id (your transaction ID) as your browser-side tag so X deduplicates.

The deduplication piece is what most people miss. If you fire browser AND server for the same purchase without a shared conversion_id, you’ll double-count purchases in X reporting. With the ID set, X takes the first event it receives and drops the duplicate — usually server wins because browser is slower and lossier.

If you want a deeper look at server-side architecture patterns we use, our GA4 setup service page covers the sGTM approach we deploy for e-commerce clients.

Common Mistakes and Troubleshooting

Firing the base tag on every page including checkout, and the event tag on the same page. Both fire, but if the base tag hasn’t finished loading the pixel library before the event tag runs, the event drops silently. Make sure event tags have Tag Sequencing configured to fire the base tag first, or use a trigger delay of a few seconds on the event.

Passing the raw email in user data despite the template hashing. Symptom: X reports “Invalid hash format” in the events log. Cause: you added SHA-256 hashing in a Custom JS variable AND the template is hashing again. Pick one. My recommendation: let the template handle it, pass raw values in.

Missing Conversion ID. Symptom: purchase counts in X Ads Manager are roughly 2x actual orders. Cause: browser and server are both firing without a shared ID. Fix: always pass transaction_id as the Conversion ID field.

Test events appearing but production events not. Nine times out of ten this is Consent Mode. In production, users deny marketing cookies more often than you think. Check your Consent Mode implementation with a fresh incognito session, deny all cookies, and see whether your CMP is properly setting ad_storage: denied (which is correct) or somehow blocking your test entirely.

Match quality stuck at Low. You’re only passing email. Add phone (properly normalised), external ID, and any address fields you have access to on the confirmation page.

Currency mismatch warnings. X validates currency codes against ISO 4217. If your dataLayer pushes £ or pounds or lowercase gbp, the event ingests but with no currency. Always uppercase ISO codes: GBP, USD, EUR.

Key Takeaways

  • The official X GTM template makes Custom HTML pixel implementations obsolete. Migrate any account still using the old approach — you’ll get better consent handling, automatic hashing, and native enhanced conversions support.
  • Always pass a Conversion ID (usually your transaction ID) so browser and server events deduplicate cleanly. Skipping this is the number one cause of inflated purchase counts.
  • User-provided data is where match quality lives. Email alone gets you Low match quality; email + phone + external ID gets you High or Excellent, and recovers 20–40% of iOS attribution.
  • Let the template hash user data. Don’t pre-hash in a Custom JavaScript variable — you’ll double-hash or send the wrong format.
  • Configure Consent Mode v2 gating on both base and event tags. The template respects denied signals natively and won’t fire redacted pings.
  • For maximum attribution recovery, run browser and server side-by-side with a shared Conversion ID. sGTM forwarding to X’s Conversions API is the single biggest lift for privacy-restricted traffic.
#GTM#X Ads#Conversion Tracking#Server-Side

Share this article

Want This Implemented Correctly?

Let our team apply these concepts to your specific setup — with QA validation and 30 days of support.