Skip to content
← ALL WRITING

2026-04-22 / 11 MIN READ

Event_id strategy across Shopify Pixel, CAPI, and GTM

A shopify event_id strategy that holds across Pixel, CAPI, and GTM. Five steps with Liquid, GTM web, and server snippets from a Q2 2024 rebuild.

In Q2 2024 I rebuilt the CAPI stack on a Shopify DTC brand. Match quality started at 6.2 and settled at 9.1. The single highest-leverage fix was a shared event_id that the browser pixel, the server-side CAPI event, and the GTM server container all agreed on. Nothing else moves the dedup chart like this one thing.

A shopify event_id strategy is not one key on one event. It is a taxonomy that travels across every surface that fires a Meta event. When any two of those surfaces disagree on the value, Meta double-counts for 48 hours and then silently collapses it. Your ROAS chart inverts on day three and your Slack lights up.

This walkthrough is the five steps I still ship on every new Shopify CAPI engagement, with the exact Liquid, dataLayer, and GTM server snippets I use.

computeechofire
SourceThemeGTM webGTM serverMetaorder.id + minute_bucketsha256() -> event_iddataLayer.push({ event_id })pixel fire: eventIDwebhook w/ event_idCAPI: event_idbrowser path / server path
One event_id computed in the theme, echoed through the dataLayer and the webhook, fires identically from both surfaces to Meta.

Prerequisites

You need four things in place before this walkthrough is useful.

A Shopify theme you can edit. Dawn-based or a custom 2.0 theme both work. You will add two Liquid snippets and one change to customer-events.js (or equivalent web pixel extension).

A GTM web container and a GTM server container. If you do not have a server container yet, start with the field guide to Meta CAPI for DTC operators and come back after the Stape or GCP setup is done.

A Meta Pixel ID and a Test Events code. The verification step at the end leans on the Test Events tool; without the code you cannot confirm dedup before routing real traffic.

SHA-256 utility available both server-side and in the theme. Shopify Liquid cannot hash natively, so you will hash in the theme using a tiny Web Crypto call and echo the value to the dataLayer.

Step 1: Pick one shopify event_id strategy namespace

Every event_id across the entire funnel must derive from the same three inputs: an event-scoped entity id, an event name, and a timestamp rounded to a shared granularity.

I use this shape:

event_id = sha256(`${entity_id}:${event_name}:${minute_bucket}`)
  • entity_id is cart.token for cart events, checkout.token for checkout events, and order.id for Purchase. Using customer_id alone is a mistake; a guest cart has no customer and the browser fires AddToCart for guests constantly.
  • event_name is the lowercase Meta event: viewcontent, addtocart, initiatecheckout, purchase.
  • minute_bucket is Math.floor(Date.now() / 60000). Seconds diverge between surfaces because browsers queue events and server webhooks arrive late. Minute buckets are forgiving enough to survive a 30-second drift and tight enough that Meta's own dedup window never gets confused.

Step 2: Generate event_id server-side, not in the browser

This is the step most Shopify implementations skip, and it is why so many brands see match quality plateau around 7.

Generate the event_id in the server of truth for each entity, then propagate it outward. For purchase events, that server is Shopify itself (via the Liquid order context or the orders/create webhook). For cart events, it is your GTM server container or your own Shopify-connected backend.

The smallest working path inside a theme, for the Purchase event on order-status-liquid:

{% if first_time_accessed %}
  <script>
    (async () => {
      const raw = "{{ order.id }}:purchase:" +
                  Math.floor(Date.now() / 60000);
      const buf = new TextEncoder().encode(raw);
      const hash = await crypto.subtle.digest("SHA-256", buf);
      const hex = [...new Uint8Array(hash)]
        .map(b => b.toString(16).padStart(2, "0")).join("");
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({
        event: "purchase_event_id_ready",
        event_id: hex,
        shopify_order_id: "{{ order.id }}"
      });
    })();
  </script>
{% endif %}

For ViewContent, AddToCart, and InitiateCheckout, do the same thing inside customer-events.js (the Shopify Web Pixel API), but seed the hash from event.data.cartLine.merchandise.id or event.data.checkout.token rather than order.id.

The important part is not the hash itself. It is that the hash is generated once, in a place that the subsequent pixel fire, server-side event, and GTM server claim can all reach.

Step 3: Echo event_id into the dataLayer for GTM web

GTM web needs the event_id as a variable it can pass to the Facebook Pixel tag. The dataLayer push from step 2 is what feeds that variable.

In GTM web:

  1. Create a Data Layer Variable named dlv.event_id pointing at event_id.
  2. Open the Facebook Pixel tag. In its Event Parameters, add a custom parameter with name event_id and value {{dlv.event_id}}.
  3. Set the tag's firing trigger to wait for the purchase_event_id_ready custom event, not for the default Shopify order-status page load.

That ordering matters. If the pixel fires on DOM ready, it will race the async SHA-256 hash and sometimes fire with an empty event_id. Waiting for the custom event guarantees the hash exists before the pixel leaves the browser.

The Shopify-specific wiring details for the Web Pixel API path (for cart events) live in a deeper walkthrough on Pixel and CAPI deduplication. This article assumes you have that lower plumbing done.

Step 4: Read event_id in GTM server and forward to Meta

In the GTM server container, your Facebook Conversions API tag needs to read the event_id from the incoming Shopify webhook payload, not regenerate it. If the server container hashes a new event_id on its own, you have defeated the entire strategy.

The Shopify webhook payload for orders/create does not carry your event_id by default. You need to add it. Two options:

Option A. A Shopify Flow action that POSTs the order plus your event_id hash to your server. Flow can call an HTTP endpoint with a body you control. The endpoint hashes order_id, minute_bucket, and the event_name, then forwards the payload to your GTM server's /data endpoint as a server_event.

Option B. An orders/create webhook pointed directly at a small serverless function that does the same hashing and then forwards to GTM server. Works the same; gives you more control over retries.

Inside GTM server, your CAPI tag reads:

Event Data Override -> event_id = {{dlv.event_id}}

Where dlv.event_id is a Data Layer Variable the tag reads from the incoming event. Confirm the tag's "Server Event Data" shows an event_id property with your hash before you enable it.

Step 5: Verify with Meta Test Events before flipping traffic

Never route production traffic through a new event_id strategy without verifying dedup first.

Open Meta Events Manager, open your Pixel, and click Test Events. Enter the test code. Then place a test order against your store with the web pixel active AND the server container firing. Two things can happen.

Single entry with both signals attached. You are clean. The browser pixel and the CAPI event hashed to the same event_id and Meta deduplicated them. Ship it.

Two separate entries for the same action. Your hash is wrong. Inspect both payloads. The three usual suspects: different minute_bucket values (hash excluded the timestamp or drifted across surfaces), different entity_id (browser used cart.token but server used checkout.token), or one surface is using the wrong casing of the key.

Do not move to production until Test Events shows a single collapsed entry for every event type you fire. The debugging postmortem on CAPI payload mismatch walks through what happens when this step gets skipped in a hurry.

Common mistakes

Regenerating the event_id per surface. The browser pixel hashes the current URL, the server hashes order_id, the GTM server container hashes something else. Three different hashes, three different events in Meta. Pick one derivation and enforce it everywhere.

Using new Date().toISOString() as the timestamp input. Browsers and servers drift by seconds. If your hash includes new Date() at full precision, surfaces almost never agree. Minute buckets fix it; excluding timestamps entirely also works.

Forgetting event_id on post-purchase upsell offers. Shopify's post-purchase page fires a second Purchase-adjacent event if you have one-click upsells enabled. If that event has no event_id, it counts fresh. On a store doing 20 percent upsell attach, that is 20 percent of revenue reported twice.

Letting the browser race the async hash. If the pixel fires before the SHA-256 digest resolves, event_id is undefined. Trigger the pixel on a custom dataLayer event, not on page load.

What to try next

Extend this strategy to Klaviyo flows. Klaviyo's Placed Order event can include your event_id if you pass it as a custom property on the Shopify-Klaviyo sync. The tutorial on CAPI for subscription commerce covers a parallel pattern for Recharge rebills, which need their own event_id namespace so rebills do not collapse into first-order dedup.

If you are mid-migration and your attribution windows changed because of iOS or Android updates, pair this with the field notes on attribution windows after iOS and Android privacy updates. A new event_id strategy plus the wrong attribution window is a regression that looks like a dedup bug.

The fastest way to know whether any of this is costing you right now is a CAPI Leak Report scan on your live store. It fingerprints the event_id strategy in under 10 seconds and flags the specific surfaces that disagree.

FAQ

Can I use Shopify's order id directly as the event_id?

Yes for Purchase events. No for cart and checkout events, because those fire for guest visitors before an order exists. Use cart.token for AddToCart and ViewContent, checkout.token for InitiateCheckout, and order.id for Purchase. Then hash all of them with the same algorithm so the key space is uniform.

Does Meta require a hashed event_id, or will a plain UUID work?

Meta accepts any string under 40 characters, so a UUID works. The reason I hash is reproducibility: the browser pixel and the server webhook can independently derive the same event_id from the same inputs without sharing state. A UUID requires the UUID to be generated in one place and read in the other, which adds a synchronization point the hash removes.

What happens if the GTM server container regenerates the event_id?

Dedup breaks. The browser pixel fires with hash A, the server container fires with hash B, Meta counts both separately. You see a 30 to 60 percent revenue inflation on day one, then Meta catches the duplicates and collapses them on day three. The ROAS chart inverts and your media buyer loses trust in the data. Configure the server tag to read the event_id from the incoming payload, never to generate it fresh.

How do I handle refunded orders so Meta does not count the original conversion?

Fire a Refund event with the same event_id as the original Purchase. Meta will offset the revenue attribution. Some brands skip this because refunds are rare, but if your refund rate is above 3 percent it is worth wiring; the attribution noise compounds over a quarter.

Does this strategy work on headless Shopify or only classic Liquid themes?

Both. On headless storefronts, the event_id generation moves from Liquid into your Next.js or Hydrogen server code. The principle is identical: one derivation, reachable from every surface that fires a Meta event. The Liquid snippet in step 2 becomes a getServerSideProps or a route handler.

Pick one derivation and enforce it everywhere. Browsers and servers drift by seconds, so minute buckets or excluded timestamps are the only values that survive.

Sources and specifics

  • The Q2 2024 rebuild referenced above was delivered for a mid-market Shopify DTC brand. Pre-rebuild match quality 6.2, post-rebuild 9.1. See the tracking gap case study for production details.
  • The SHA-256 plus minute-bucket derivation is the exact shape I have shipped on every Shopify CAPI engagement since Q2 2024.
  • The Test Events verification step is from Meta's public Events Manager tooling, available to any Pixel administrator.
  • Liquid snippet tested on Shopify 2.0 themes with the Web Pixel API enabled; the same pattern works on Dawn and on custom themes with minor path adjustments.
  • The common-mistakes list is drawn from five DTC audits where event_id strategy was the primary attribution failure, not from public benchmarks.

// related

DTC Stack Audit

If this resonated, the audit covers your tracking layer end-to-end. Server-side CAPI, dedup logic, and attribution gaps - all mapped to your stack.

>See what is covered