Skip to content
Lapis
Esc
navigateopen⌘Jpreview
On this page

Shopify

Install the Lapis pixel in theme.liquid, persist the click ID as a cart attribute, and send orders from the orders/create webhook.

Shopify locks down checkout, so the reliable pattern is: cookie, then cart attribute, then order note attribute, then webhook. Shopify carries cart attributes onto the order automatically, so you need no checkout customization.

1. Install the pixel

In Shopify admin, go to Online Store → Themes → ⋯ → Edit code → layout/theme.liquid. Add this before the closing </body> tag:

<script
  src="https://cdn.trylapis.com/pixel/v1/lapis-pixel.js"
  data-key="pk_YOUR_PIXEL_KEY"
></script>

2. Persist the click ID as a cart attribute

Directly below the pixel script, add:

<script>
(function () {
  function getLapisClickId() {
    var cookie = document.cookie.split("; ").find(function (entry) {
      return entry.indexOf("lapis_sid=") === 0;
    });
    if (!cookie) return null;
    try {
      var session = JSON.parse(
        decodeURIComponent(cookie.split("=").slice(1).join("="))
      );
      return session.sid || null;
    } catch (e) { return null; }
  }

  var clickId = getLapisClickId();
  if (!clickId) return; // organic visitor, so do nothing

  fetch("/cart/update.js", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ attributes: { lapis_click_id: clickId } }),
  }).catch(function () {});
})();
</script>

This is idempotent, so it is safe to run on every page view. The attribute lands on the order as note_attributes.lapis_click_id.

3. Send orders from the webhook

Create an orders/create webhook that points at your backend (Settings → Notifications → Webhooks, or via the Admin API), then:

// POST /webhooks/shopify/orders-create
app.post("/webhooks/shopify/orders-create", async (req, res) => {
  res.sendStatus(200); // ack Shopify first, because it retries slow responses

  const order = req.body;
  const attrs = Object.fromEntries(
    (order.note_attributes || []).map((a) => [a.name, a.value])
  );
  if (!attrs.lapis_click_id) return; // organic order, so skip

  await fetch("https://api.trylapis.com/api/v1/pixel/server-events", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.LAPIS_SERVER_EVENT_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      event_id: `order_created:${order.id}`,
      event_type: "order_created",
      lapis_click_id: attrs.lapis_click_id,
      value: parseFloat(order.total_price),
      currency: order.currency || "USD",
      properties: { order_number: String(order.order_number) },
    }),
  });
});

Tip: Shopify retries webhooks aggressively. Keying event_id to order.id makes every retry deduplicate, so you never double-count.

Shopify-specific notes

  • Republish after editing the theme. Changes to theme.liquid on the live theme go out immediately, but if you edit an unpublished theme, the live one stays unchanged.
  • checkout_started: use the checkouts/create webhook with event_id: "checkout_started:" + checkout.token. The cart attribute is already on the checkout object.
  • Shopify Plus and checkout extensibility: the cart-attribute pattern works unchanged. You do not need checkout.liquid access.
  • Headless Shopify (Hydrogen): treat it as custom code. Set the attribute via the Storefront API cartAttributesUpdate mutation instead of /cart/update.js.
  • No app required: this is intentionally a theme-code integration. There is no Lapis Shopify app to install or update.

Full event details: Ecommerce and Shopify events.

Was this page helpful?