Shopify install guide
Snippet in theme.liquid, visitor id carried through the cart, and an orders/paid webhook that confirms every sale from Shopify's servers rather than the shopper's browser.
theme.liquid snippet + cart attribute + orders/paid webhook
The hard part on Shopify is that checkout runs on a different origin, so storage written on your store domain is unreadable there. The fix is to carry the visitor id into the order as a cart attribute and read it back from the webhook.
The orders/paid webhook fires from Shopify's servers — ad blockers, consent banners and checkout restrictions are irrelevant to it.
1. The snippet
Online Store → Themes → Edit code → layout/theme.liquid, immediately before </head>:
<script async src="https://flowsk.com/flowsk.js" data-write-key="pk_live_…"></script>
Put it above app-injected scripts. Early placement means the landing URL, UTMs and fbclid/gclid are captured before a consent banner or app can rewrite them.
2. Carry the id into the cart
Checkout runs on checkout.shopify.com or shop.app, not your domain, so nothing you wrote to storage on the storefront is readable there. Attach the id to the cart instead — it travels with the order.
<script>
document.addEventListener("DOMContentLoaded", function () {
if (!window.flowsk) return;
fetch("/cart/update.js", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ attributes: { fsk_id: window.flowsk.anonymousId() } })
}).catch(function () {});
});
</script>
The attribute now appears on the order as a note attribute, which your webhook can read.
3. Confirm the sale from the webhook
Settings → Notifications → Webhooks → Create webhook, event Order payment, format JSON, pointed at your backend.
# Your backend, wherever it lives
post "/webhooks/shopify/orders" do
order = JSON.parse(request.body.read)
fsk_id = order["note_attributes"]&.find { |a| a["name"] == "fsk_id" }&.dig("value")
Net::HTTP.post(
URI("https://flowsk.com/api/events"),
{
write_key: ENV["FLOWSK_WRITE_KEY"],
anonymous_id: fsk_id,
events: [{
source: "server",
category: "purchase",
name: "purchase",
email: order["email"],
value_cents: (order["total_price"].to_f * 100).round,
dedup_key: "shopify_order_#{order['id']}"
}]
}.to_json,
"Content-Type" => "application/json"
)
status 200
end
Use orders/paid, not orders/create — the latter fires for orders that are never actually paid.
4. Identify at every email capture
Newsletter popup, back-in-stock, account creation, checkout. Anywhere an email legitimately arrives:
flowsk.identify(email, { via: "newsletter" })
On Shopify this is the highest-leverage change available, because DTC consideration routinely exceeds the seven days a JavaScript identity survives on Safari. If the email arrives on day 2, the day-11 purchase is still attributed correctly.
For a logged-in customer, identify from Liquid:
{% if customer %}
<script>flowsk.identify({{ customer.email | json }}, { via: "account" });</script>
{% endif %}
5. Make the id durable (optional, high value)
Shopify does not let you set a cookie from its servers, so the fully durable version needs a small endpoint of your own — a Cloudflare Worker in front of the store, or a serverless function on a subdomain — that mints _fsk_id and sets it in a response header. See proxy setup.
Without it you get the standard seven-day JavaScript lifetime, mitigated by step 4. With it, the whole consideration window is measurable.
Shopify-specific gotchas
App-injected tags load late. Theme app extensions frequently fire after first paint and miss the landing referrer entirely. Keeping the snippet first in theme.liquid sidesteps this.
Shopify’s own analytics is last-click. Its acquisition reports use a different model and will not agree with either your ad platforms or your first-party data. Pick one ledger for decisions.
Discount and gift-card orders. total_price is what was actually charged, which is what you want for ROAS. If you would rather credit gross value, use subtotal_price — just be consistent.
Test orders. Filter them out before sending, or your first week of data will have a suspiciously good conversion rate.
Frequently asked questions
Where do I put the snippet?
layout/theme.liquid, immediately before </head>, above any app-injected scripts. Being first means you capture the landing URL and click ids before anything else can alter them.
Does this work with checkout extensibility?
Yes. The webhook path is unaffected by checkout changes, which is precisely why the server-side confirmation is the durable option — Shopify keeps restricting client-side scripts in checkout, and this does not depend on them.
Do I need Shopify Plus?
No. The cart-attribute plus webhook approach works on every plan. Plus only matters if you want scripts running inside checkout itself, which this design deliberately avoids needing.
What about subscription apps?
Send each recurring charge as its own purchase with the invoice or charge id as the dedup key, so cohort revenue accrues to the acquiring campaign rather than just the first order.
Will this slow my store down?
The snippet is a few KB and async. The webhook runs on Shopify's infrastructure and never touches the shopper's page.
Check what your theme is actually loading
Scan your storefront: every third-party script an app has injected, and whether anything sets a durable first-party id.
Stop guessing which ad made the sale.
Flowsk Signals stitches the anonymous click to the email to the purchase — first-party, server-side, de-duplicated. One snippet, $29/mo, and every conversion comes with a receipt you can inspect.