How to Install PostHog on Shopify and Track Checkout Events

A consent-controlled cookie joins storefront and checkout under one visitor ID on the same host. Compare the IDs before trusting a purchase funnel.

A Shopify store window split by a doorway into Storefront and Checkout: the storefront's header strip is lit blue by the PostHog snippet and stops at the doorway. Below, a sealed box labelled Custom pixel collects checkout events. Two separate lines carry the streams to the PostHog logo.

Shopify’s analytics section shows sales and traffic trends. When we investigate why a store isn’t converting, we also want to follow what visitors do before they leave.

We first wrote this guide after finding gaps while installing PostHog on a Shopify store. PostHog now documents a theme snippet and a checkout_completed pixel. Here we also cover cart and checkout steps, consent, and how to give storefront and checkout events the same visitor ID.

Before you start, you’ll need:

  • A PostHog account, organization and project, with its project token and ingestion host.
  • A Shopify admin or staff account with access to duplicate and edit themes and manage Customer events, plus a way to place a test order.

In this guide

Quick answer

Add the PostHog web snippet to theme.liquid for storefront pageviews and interactions, then add a Customer events custom pixel for native cart and checkout events. Checkout runs outside theme JavaScript. (+) Pass the storefront visitor ID to the pixel, then verify both event delivery and identity in PostHog Activity.

Step 1: Grab the PostHog snippet

Copy the PostHog snippet from your project’s settings:

  1. Log in to your PostHog account.
  2. Navigate to Project Settings.
  3. Find the Web snippet section and copy the HTML snippet provided.

Keep person_profiles: 'identified_only', the default. It captures anonymous events and creates a person profile when you identify someone. (+) Switching to always makes every visitor’s events identified without adding event coverage; for an anonymous storefront, that adds profile processing and can raise the bill. See how identified events affect PostHog costs.

Step 2: Inject the Snippet into Your Shopify Theme

Use an unpublished duplicate to test the installation, and coordinate publication with anyone else editing the theme. Check for an existing PostHog app, theme snippet or GTM installation before adding another copy.

  1. In Shopify, open Online Store → Themes.
  2. Click the three dots beside the current theme.
  3. Click Duplicate.
  4. Open the three-dot menu on the duplicate.
  5. Click Edit code.
Shopify Themes page with the Edit code menu option highlighted for the current theme
2024 Shopify admin: the theme menu, including Duplicate and Edit code; store details redacted.

In the left sidebar, click layout/theme.liquid. Prepare the consent handling below, then paste the snippet before the closing </head> tag.

The theme snippet is outside the pixel’s privacy controls. Use Shopify’s Customer Privacy API to check analyticsProcessingAllowed() and listen for visitorConsentCollected; connect changes to PostHog’s opt-in and opt-out controls.

Inside the copied snippet’s <script> tags, keep the loader and replace its posthog.init(...) call with this JavaScript. Use the same project token and api_host as your copied snippet, retain its defaults option, and combine any existing loaded callback with the one below.

// Keep the loader from your project snippet above this code.
window.analyticsConsent = new Promise(resolve => {
  window.allowAnalytics = resolve;
});

window.Shopify.loadFeatures([
  { name: 'consent-tracking-api', version: '0.2' },
], error => {
  if (error) return;
  const privacy = window.Shopify.customerPrivacy;
  let client;
  let started = false;
  let pageviewSent = false;

  window.refreshPostHogBridge = function () {
    if (!client || !privacy.analyticsProcessingAllowed()
        || client.has_opted_out_capturing()) return;
    document.cookie = 'ph_storefront_id='
      + encodeURIComponent(client.get_distinct_id())
      + '; Path=/; SameSite=Lax; Secure';
  };

  function syncConsent() {
    if (!privacy.analyticsProcessingAllowed()) {
      if (client) client.opt_out_capturing();
      document.cookie = 'ph_storefront_id=; Max-Age=0; Path=/; SameSite=Lax; Secure';
      return;
    }
    if (!started) {
      started = true;
      posthog.init('YOUR_POSTHOG_PROJECT_TOKEN', {
        api_host: 'https://eu.i.posthog.com', // Use your project's host.
        // Retain the defaults option from your copied snippet here.
        person_profiles: 'identified_only',
        opt_out_capturing_by_default: true,
        opt_out_persistence_by_default: true,
        capture_pageview: false,
        loaded: function (ph) {
          client = ph;
          syncConsent();
        },
      });
      return;
    }
    if (!client) return;
    if (client.has_opted_out_capturing()) client.opt_in_capturing();
    window.allowAnalytics();
    window.refreshPostHogBridge();
    if (!pageviewSent) {
      client.capture('$pageview');
      pageviewSent = true;
    }
  }

  document.addEventListener('visitorConsentCollected', syncConsent);
  syncConsent();
});

This starts PostHog when Shopify allows analytics, writes the current visitor ID to ph_storefront_id from the loaded callback, and clears that cookie when permission is withdrawn. The callback sends the first $pageview after permission is available; automatic pageviews are off to avoid counting it twice. With opt_out_persistence_by_default: true, withdrawal also removes PostHog’s cookies and localStorage data; the consent choice and a per-tab window ID in sessionStorage remain. (+) window.analyticsConsent resolves once capture is allowed, so a page experiment can wait for it, as our Shopify A/B testing guide shows. The example assumes normal theme page loads and an HTTPS storefront.

Shopify theme code editor with layout/theme.liquid open and an earlier PostHog snippet highlighted
2024 Shopify admin: an earlier snippet near the opening head tag, with store identifiers redacted. Use the closing head tag and consent handling above.
  1. Click Save.
  2. Open the duplicate’s Preview.
  3. Allow analytics and check for $pageview in PostHog Activity.
  4. Keep the theme unpublished until the checkout checks in Step 4 pass.

Step 3: Capture Shopify Events in PostHog

Use Shopify’s Customer events to capture actions such as checkout_completed and checkout_started. These tell you whether visitors reached checkout and completed it.

We start with four events: product_viewed, product_added_to_cart, checkout_started and checkout_completed. They let us follow a product view through cart, checkout and purchase. The theme already measures pageviews; product_viewed also supplies the product variant. (+) PostHog recommends pixels only for conversions and warns of CORS errors with other SDK-based pixel events. (+) We send these selected events directly to the capture API and test them below; add another event when it answers a specific question.

  1. In your Shopify admin dashboard, click Settings.
  2. Select Customer events.
  3. Click Add custom pixel.
  4. Enter a name for your pixel, such as PostHog.
  5. Click Add pixel to open the editor.

Under Customer privacy → Permission, select Required and Analytics. The 2024 screenshot also selects Data collected does not qualify as data sale; use that choice only if it describes your use of the data. These settings control when the pixel collects data. (+) Customer events configuration is store-wide: a duplicated theme does not isolate pixel changes, so use a development store to test changes that could affect live tracking.

Shopify Customer events settings for the PostHog pixel with Analytics permission selected
2024 Shopify admin: Analytics is required; the selected data-sale option describes this installation, not every store. Store details redacted.

Paste the JavaScript below into the pixel’s Code window. Replace YOUR_POSTHOG_PROJECT_TOKEN in POSTHOG_PROJECT_KEY with your project token, and set POSTHOG_HOST to the same ingestion host used in the theme. US Cloud uses https://us.i.posthog.com; EU Cloud uses https://eu.i.posthog.com. (+)

Give both layers the same visitor ID

A custom pixel runs in a sandboxed iframe that cannot access the top frame, so a PostHog SDK inside it cannot read the storefront’s cookie directly. (+) The consent code in Step 2 must already be in your theme: it writes a cookie with the visitor ID. The pixel reads it through Shopify’s browser.cookie.get(), which runs in the top frame, then sends that ID with each event. (+)

const POSTHOG_PROJECT_KEY = 'YOUR_POSTHOG_PROJECT_TOKEN';
const POSTHOG_HOST = 'https://eu.i.posthog.com'; // US: https://us.i.posthog.com

let privacy = init.customerPrivacy;
api.customerPrivacy.subscribe('visitorConsentCollected', event => {
  privacy = event.customerPrivacy;
});

async function sendToPostHog(event) {
  if (privacy?.analyticsProcessingAllowed !== true || !event.clientId) return;

  const location = event.context?.document?.location;
  if (!location?.href) return;
  const url = new URL(location.href);
  const checkout = event.data?.checkout;
  const variant = event.data?.productVariant
    ?? event.data?.cartLine?.merchandise;

  let bridgedId = '';
  try {
    const encodedId = await browser.cookie.get('ph_storefront_id');
    bridgedId = encodedId ? decodeURIComponent(encodedId) : '';
  } catch (_) {
    // A missing or unreadable cookie leaves an explicitly separate stream.
  }
  // Consent can change while the asynchronous cookie read is pending.
  if (privacy?.analyticsProcessingAllowed !== true) return;

  const properties = {
    $process_person_profile: false,
    $current_url: url.origin + url.pathname,
    $host: url.host,
    $pathname: url.pathname,
    event_source: 'shopify_custom_pixel',
    identity_source: bridgedId ? 'storefront_bridge' : 'shopify_client_id',
    shopify_event_id: event.id,
  };
  if (variant?.id) properties.product_variant_id = variant.id;
  if (checkout?.token) properties.checkout_token = checkout.token;
  if (checkout?.order?.id) properties.order_id = checkout.order.id;
  if (checkout?.currencyCode) properties.currency = checkout.currencyCode;
  if (checkout?.totalPrice?.amount != null) {
    const value = Number(checkout.totalPrice.amount);
    if (Number.isFinite(value)) properties.checkout_total = value;
  }

  const response = await fetch(POSTHOG_HOST + '/i/v0/e/', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    keepalive: true,
    body: JSON.stringify({
      api_key: POSTHOG_PROJECT_KEY,
      distinct_id: bridgedId || ('shopify:' + event.clientId),
      event: event.name,
      timestamp: event.timestamp,
      properties,
    }),
  });
  if (!response.ok) throw new Error('PostHog capture failed: ' + response.status);
}

for (const name of [
  'product_viewed',
  'product_added_to_cart',
  'checkout_started',
  'checkout_completed',
]) {
  analytics.subscribe(name, event => {
    sendToPostHog(event).catch(error => console.error(error.message));
  });
}

Click Save. Then click Connect to start tracking events.

Shopify Customer events page showing the PostHog custom pixel marked Connected
2024 Shopify admin: the PostHog custom pixel after connection; store details and the store’s other apps blurred.

The pixel preserves Shopify’s event time and page context. When available, it sends the variant ID, checkout token, order ID, currency and numeric checkout total, along with the event and identity fields shown in the code. It sends no buyer email, names, addresses, URL query strings or fragments. Use shopify_event_id to trace delivery; it does not prevent duplicates. Failed requests are logged but not retried.

We set $process_person_profile: false to prevent the API events from creating profiles. Events are anonymous when their ID is not linked to a profile. If the ID already belongs to a person, they still count as identified events, even though this setting prevents profile updates. (+)

The bridge uses a host-only session cookie: checkout on another host, including a subdomain, cannot read it. (+) A missing or unreadable cookie uses shopify: plus clientId, marked by identity_source: 'shopify_client_id'; keep those unjoined events out of a storefront purchase funnel. Consent withdrawal must stop capture and clear the cookie, as the callback does; after identify() changes the storefront ID, call window.refreshPostHogBridge() to update it. (+) The bridge carries the visitor ID, not the session: checkout events have no $session_id, so they do not join the storefront’s session or recording. (+)

Choose whether to identify buyers

For anonymous shoppers, our bridge joins storefront and checkout using an opaque visitor ID, without adding buyer names, email or addresses. If you need buyer history across devices and sessions, you can also link each buyer’s email to that visitor ID when the order completes. This pixel has no PostHog SDK, so the link is an alias event sent to the capture API: the email becomes the person’s distinct_id, and the storefront ID is merged into it as the alias. (+) Add this below the pixel code only if you may send buyer email addresses to PostHog, and expect that visitor’s later events to count as identified events.

// Optional: link the buyer's email to the storefront visitor ID.
// Use only if you may send buyer email addresses to PostHog and the theme never calls identify().
analytics.subscribe('checkout_completed', async event => {
  if (privacy?.analyticsProcessingAllowed !== true) return;
  const email = event.data?.checkout?.email?.trim().toLowerCase();
  let storefrontId = '';
  try {
    storefrontId = decodeURIComponent(await browser.cookie.get('ph_storefront_id') || '');
  } catch (_) {}
  // Consent can change while the cookie read is pending.
  if (privacy?.analyticsProcessingAllowed !== true) return;
  if (!email || !storefrontId || storefrontId === email) return;

  try {
    const response = await fetch(POSTHOG_HOST + '/i/v0/e/', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      keepalive: true,
      body: JSON.stringify({
        api_key: POSTHOG_PROJECT_KEY,
        event: '$create_alias',
        distinct_id: email,
        properties: { alias: storefrontId },
      }),
    });
    if (!response.ok) console.error('PostHog alias failed: ' + response.status);
  } catch (error) {
    console.error(error.message);
  }
});

PostHog will not use an ID as an alias if it is already linked to another person or was itself used to identify someone. (+) Use this handler only if the theme never calls identify(): an ID the theme has identified, such as a customer ID, cannot then be aliased to the email. If your theme identifies logged-in customers, identify buyers there instead, with one ID scheme for both. Test a new visitor’s first order, a second order from the same browser and a returning buyer on another device: each order’s events should end up on the buyer’s person. The alias does not set an email property or change the theme’s current ID.

To cover checkouts abandoned after the email step as well, repeat the subscription for checkout_contact_info_submitted; this event requires Checkout Extensibility and may arrive without an email. (+)

Alternative: subscribe to all standard events

To receive the other Shopify standard events, replace the final for loop with this subscription. Keep sendToPostHog and its property allowlist unchanged. This all_standard_events subscription excludes page_viewed and collection_viewed, because the theme already measures storefront visits as $pageview. Shopify also emits page_viewed on checkout and order-status pages, where the theme does not run, so this filter drops those views too; remove it from the set if you need them. (+)

// Replace the final for loop above; keep sendToPostHog unchanged.
const storefrontViews = new Set(['page_viewed', 'collection_viewed']);
analytics.subscribe('all_standard_events', event => {
  if (storefrontViews.has(event.name)) return;
  sendToPostHog(event).catch(error => console.error(error.message));
});

Use one subscription version at a time. The broader version adds events such as product_removed_from_cart and payment_info_submitted, with only the properties the allowlist recognises; it does not collect custom events or every field Shopify exposes. Test the additional events and check the browser console before relying on them.

Step 4: Verify Installation

Perform the actions below on your store. In PostHog, open Activity and filter by each event’s exact name. Allow a few minutes for the events to appear.

ActionWhat to see in PostHog ActivityWhat to check
Visit the storefront$pageview from the themeOne event for the page load, after analytics is allowed.
View a productproduct_viewedThe expected product_variant_id.
Add it to the cartproduct_added_to_cartOne event for the action, with the right variant.
Start checkoutcheckout_startedThe original event time, checkout token, currency and total.
Complete a test checkoutcheckout_completedThe order ID, value, currency and storefront visitor ID.
Decline or withdraw analytics permissionNo further events from either layer while analytics is disallowedThe bridge cookie is removed and new actions send no events.

For the same test journey, run posthog.get_distinct_id() in the storefront’s browser console and compare it with distinct_id on the pixel events in PostHog. They must match for this bridge to work. If identity_source is shopify_client_id, check the cookie, its host and whether the storefront SDK had loaded before the event. Repeat on mobile and on the actual checkout host.

PostHog Events page showing Shopify events including checkout_completed and product_added_to_cart
PostHog Events from our 2024 Shopify installation, with account details redacted. The earlier pixel sent additional events, including collection_viewed.

If events are missing, check that the pixel is connected, analytics is allowed, and the token and host match the theme. Inspect the browser’s network requests and console for delivery errors, then find the event in Activity: an HTTP success response alone does not prove ingestion. (+)

checkout_completed fires once per checkout, normally on the Thank you page or on the first upsell page when there is a post-purchase upsell. (+) It does not fire again on the Thank you page, and it is absent if its trigger page fails to load. Reconcile purchases against Shopify orders; when completeness matters, add an order-webhook pipeline with reconciliation, rather than relying on browser delivery alone.

Compare completed orders by order_id, and keep checkout_total with its currency. For stores selling in several currencies, convert currencies before combining revenue in PostHog.

Once these checks pass, publish the tested duplicate from Online Store → Themes, then repeat a storefront-to-checkout journey on the live theme.

What the Theme Layer Needs on Shopify

Capture which product a shopper clicks

Add data-ph-capture-attribute-* attributes to your existing add-to-cart button to include product details in its autocapture event. (+) These Liquid attributes capture the product’s title and ID:

data-ph-capture-attribute-product-name="{{ product.title | escape }}"
data-ph-capture-attribute-product-id="{{ product.id }}"

That event describes the click. Use product_added_to_cart to measure the cart addition; a button click alone does not establish that the cart changed. (+)

Make heatmaps and the toolbar visible

Shopify’s frame-ancestors Content Security Policy blocks the embedded heatmap view. Use a Cloudflare Worker on a separate subdomain to adjust the framing policy for that view, following PostHog’s worker example; keep the production store’s policy separate.

If the toolbar is invisible, check whether the theme hides empty div elements. (+) Remove div:empty from the theme’s display: none selector list.

Keep replay in the storefront

If you use an SDK-based pixel, set disable_session_recording: true in that pixel’s posthog.init. Record the storefront through the theme; the capture-API pixel above starts no recorder.

Consent still applies to the theme layer. Shopify’s analytics permission combines the visitor’s choice, location and merchant settings. (+) If you use a third-party banner, make sure it passes choices to Shopify’s Customer Privacy API so both layers act on the same permission.

Shopify App Instead?

PixieHog by CeladonWorks is free and works with checkout. (+) Its default pixel events cover page views, cart and checkout; product views are off by default, and session replay and experiments need PostHog on the storefront: turn on its optional on-site snippet unless the theme already loads PostHog. (+) PostHog does not support third-party integrations such as PixieHog. (+) To choose between the app, manual code and an order sync, use our Shopify integration comparison.

What to Do with the Data

Once purchases belong to the same visitor as storefront activity, follow our Shopify A/B testing guide. It shows how to check that a purchase follows exposure to the test.

To send verified checkout_completed events to ad platforms, follow our Google Ads conversion tracking and Meta Ads conversion tracking guides. Configure their matching fields and permissions separately; the default commerce payload omits buyer contact details and ad click IDs, and the optional identification handler sends only the email as an ID.

Use our PostHog audit to check the rest of the setup, including consent, attribution and duplicate events.

Frequently asked questions

Is the theme snippet enough to track checkout?

No. Adding code to theme.liquid does not make it run on checkout or post-purchase pages. Use a Customer events pixel for those conversions. (+)

Does this work with Hydrogen or a headless storefront?

Yes for the events emitted by Shopify-hosted checkout. Install the PostHog SDK in your storefront’s framework instead of editing theme.liquid, and instrument storefront events there. For Hydrogen, connect analytics and consent in the Hydrogen app; the pixel alone does not add product views or cart events to a headless storefront. Verify the checkout identity separately, especially when the hosts differ.

Can several stores use one PostHog project?

Yes. After consent, register a store property in the theme with posthog.register({ store: 'store-a' }) and add the same value to the pixel’s properties object. (+) You can then filter events by store. Use separate projects when the data must stay apart. (+) A shared project still needs an identity handoff if you want to follow one visitor between different domains.

Why don’t PostHog and Shopify order counts match?

Browser capture can miss purchases even when an order exists in Shopify. Compare unique completed order_id values for the same reporting period, handle test orders consistently, and check for duplicate capture from apps or other pixels. Use Shopify as the record for orders and money; a missing browser completion event does not mean the sale failed. (+)

Does PostHog have an official Shopify app?

No. PostHog offers a manual Shopify integration and lists apps such as PixieHog as third-party options. Support for an app comes from its developer.

What does this cost?

PostHog includes 1 million analytics events and 5,000 session recordings free each month, shared across an organization’s projects. (+) Usage above the free allowances is billed by product on a paid plan. Use our cost guide to estimate usage and set billing limits.

About the author

Iman Nazari

Iman Nazari

Iman combines user psychology, business strategy, and experimentation to uncover what drives action and improves performance. He focuses on hypothesis development, evidence-based decision making, and turning insights into changes that can be confidently tested and scaled.

More articles

Where is growth getting stuck?

Bring us the funnel, the evidence, and the uncertainty. We’ll help you find the leverage worth testing.

Start the conversation