Architecture

You can't get a visitor's IP address in Shopify Liquid. Here's why.

The Liquid request object exposes five properties, and none of them is the visitor's IP. Here's the architectural reason, where the IP does exist on Shopify, and what to do at each layer instead.

Bas Lefeber

Founder, learnshopify.dev · August 3, 2026 · 8 min read

Ready to learn Shopify development?Short, interactive lessons where you write real Liquid against a live storefront and watch it change as you type. Free, and genuinely fun.

It is one of the most common questions in Shopify theme development, and it has an unsatisfying answer. A merchant wants to hide a section from one problem visitor, or geo-gate a page, or log who is hammering the store. You go looking for the visitor's IP in Liquid, and it is not there. Not hidden behind a flag, not in an undocumented property. It does not exist.

The reason is architectural rather than arbitrary, and once you see it, a lot of Shopify's other constraints stop being surprising. This post covers what the request object actually holds, why the IP cannot be there, the three workarounds developers reach for and how each one fails, and where a visitor's IP genuinely is available on Shopify.

The short version

Liquid renders on a heavily cached CDN edge, and a cached page cannot contain per-visitor data. So request exposes only what is safe to cache: design_mode, host, locale, origin, and page_type. The IP exists after an order is placed (Order.clientIp in the Admin API) and inside any request your own server handles. Blocking a visitor before they see the page has to happen outside Liquid.

What the request object actually contains

The whole surface is five properties. This is it, straight from Shopify's Liquid reference:

PropertyTypeWhat it gives you
request.design_modebooleanTrue inside the theme editor. Do not use it to change customer-facing behaviour.
request.hoststringThe domain the request is hosted on.
request.originstringProtocol plus host, for building absolute URLs.
request.localeshop_localeThe locale of the request. A language, not a location.
request.page_typestringThe template being rendered: product, collection, cart...
Every property on the Liquid request object.

No IP. No country. No user agent. No headers. Notice what the five that survived have in common: every one of them is a property of the URL being requested, not of the person requesting it. That is the whole rule, and it is the tell for why the IP was never going to be there.

The reason: one render, many visitors

Shopify serves storefronts through a global CDN. When the first shopper hits /products/ethiopian-yirgacheffe, Liquid renders and the resulting HTML is cached at the edge. The next few thousand shoppers who ask for that URL get served the cached bytes without Liquid running again.

That model is why Shopify storefronts are fast. It is also strictly incompatible with per-visitor data in the template. If Liquid could write {{ request.ip }} into the page, then either the CDN caches one shopper's IP and serves it to everyone (a data leak), or the page becomes uncacheable for everybody (a performance collapse). There is no third option, so Shopify never exposed it.

One render, served to thousands. Per-visitor data in the template would either leak across visitors or kill the cache.

The generalisable rule

Anything you want to vary per visitor on a Shopify storefront has to resolve after the cached HTML is delivered (in the browser), or before it (at a layer that sits in front of the CDN). Liquid is the one place it cannot happen. This same rule explains why cart contents, customer-specific pricing, and recently-viewed products are all client-side or Section-Rendering-API concerns.

The three workarounds, and how each one fails

Search this problem and you will find three suggestions. All three are real techniques; none of them does what people usually want, and it is worth knowing precisely where each breaks.

1. Call a third-party IP API from the browser

The most-copied answer. Drop a fetch to a geo-IP service into your theme, read the response, act on it.

js
// The pattern you'll find on every forum thread.const res = await fetch("https://some-geo-api.example/json");const { ip, country } = await res.json(); if (BLOCKED.includes(country)) {  document.body.innerHTML = "Not available in your region";}

It does return an IP. What it does not do is stop anyone. The page has already been delivered in full before that fetch resolves, so the content is in the DOM, in view-source, and in the browser cache. Anyone who disables JavaScript, or reads the HTML with curl, never sees the check at all. It also adds a blocking third-party round trip to every page load, and free geo-IP endpoints are rate-limited in ways that fail unpredictably under real traffic.

This is a curtain, not a lock

Client-side checks are fine for personalisation (showing a currency hint, suggesting a market). They are not a control. If the requirement is "this visitor must not receive the content," a technique that runs after the content is delivered cannot satisfy it, no matter how it is written.

2. Put Cloudflare in front of the store

The instinct of every developer who has worked outside Shopify: park a proxy in front of it and filter there. On a Shopify-hosted storefront, you cannot. Enabling Cloudflare's proxy (the orange cloud) on a domain pointed at Shopify produces the error Your domain has a Cloudflare Proxy, which is not supported by Shopify. The DNS record has to stay on the grey cloud, DNS-only.

Shopify's stated reasoning is worth repeating, because it is not just territorial: a proxy in front of the store interferes with Shopify's ability to react to provider issues, and it alters request attributes before they reach Shopify, which degrades Shopify's own bot detection. You would be trading their bot defences for yours. Setups that appear to work are unsupported and outside the scope of Shopify Support.

3. Read it from a Shopify Function at checkout

Functions run server-side on Shopify's infrastructure, so this one feels promising. The Cart and Checkout Validation Function API is the only server-side way to validate a checkout, and it can block one. But look at what its input actually contains: the cart, the buyerJourney step, buyer identity, attributes. No IP, no network, no user agent. You can enforce rules about what is being bought and by whom, not about where the connection came from.

Where the IP does exist

Two places, both of them after or outside the cached storefront render.

1. On the order, once it exists. The Admin API exposes Order.clientIp, the IP the order was placed from. It needs the read_orders scope:

GraphQL Admin API
query OrderClientIp {  order(id: "gid://shopify/Order/1234567890") {    id    name    clientIp    customerJourneySummary {      momentsCount    }  }}

This is genuinely useful for fraud review, chargeback evidence, and spotting a cluster of orders from one network. It is useless for prevention: by the time you can read it, the order is placed and the inventory is committed.

2. On any request your own server handles. An app proxy forwards storefront requests to a server you control, and that server sees the connection like any other HTTP request. The same is true of an app embed that calls your own endpoint. This is the layer where per-visitor logic legitimately lives, and it is the layer every blocking tool on the App Store is built on, because it is the only one available.

The IP is visible to Shopify and to your own server. It is never visible to Liquid.

Learn this properly · free lesson

The shape of a theme: where everything lives

Most Shopify surprises like this one dissolve once you can place a file in the request lifecycle. Map a real theme and see what runs where. Free lesson, no signup.

Try this lesson — free

So what do you actually build?

Work backwards from the requirement, because the right layer is entirely determined by what "blocked" has to mean.

What you needLayerHow
Hint a market or currencyBrowserClient-side geo lookup. Cosmetic, and that is fine.
Stop orders shipping somewhereShipping / MarketsShipping zones and Markets. Native, free, no code.
Enforce a rule at checkoutFunctionsCart and Checkout Validation. Cart data only, no IP.
Deny the page to a networkServer / edgeYour own server, or a purpose-built app. Not Liquid.
Investigate after the factAdmin APIOrder.clientIp plus Shopify's fraud analysis.
Pick the layer from the requirement, not the other way round.

If you land on the fourth row, you are building a small distributed system: an IP-reputation data source that stays current, an edge-cached decision service fast enough to sit in a page load, a fail-open path so an outage never takes the storefront down with it, and hashed logging so you are not accumulating raw visitor IPs in a database. That is a real project, not an afternoon.

Blocking an IP without building the stack

Cordon is a Shopify app built on exactly the architecture this post describes: an app embed in the storefront, backed by a detection service that does see the connection. Because it sits at that layer, it can act on everything Liquid cannot:

  • Individual IPs and CIDR ranges. Block one address or a whole subnet, with allowlists for the addresses that must always get through.
  • Whole networks by ASN. One rule covering every IP an operator controls, which is what you actually want when the problem rotates addresses within a range.
  • Datacenter and cloud networks. AWS, Alibaba, Tencent, Huawei Cloud and similar, where scrapers and click fraud originate rather than real customers.
  • VPNs, residential proxies and Tor exits. Live detection rather than a stale IP list, which matters because the whole point of a rotating proxy is that yesterday's list is wrong.
  • Countries, 200+ of them. One click each, with presets for the common cases.
Blocking one address is the narrowest tool and the easiest to evade. The durable rules describe the connection, not the number.

The engineering details are the part worth checking on any tool in this category, because they are where these things go wrong on a live store. Decisions land in under 50ms with a storefront script under 10KB. It fails open: if the detection service is unreachable, every visitor is allowed through, so an outage on their side never becomes downtime on the merchant's. Verified search engines are exempted by reverse DNS rather than by trusting a spoofable user-agent string, and iCloud Private Relay is allowlisted by default so Apple users are not swept up by proxy rules. Visitor IPs are SHA-256 hashed with a daily-rotating salt and are irreversible after 24 hours, while the log still shows country, network, and which rule fired.

Start on the free tier

Country blocking, IP rules and bot detection are on the free plan, which is enough to confirm the mechanism works on a real store. ASN blocking starts at Starter, live VPN and proxy detection at Growth, and datacenter plus scraper detection at Pro. Paid plans carry a 7-day trial. If you want the merchant-side click-through, there is a step-by-step guide to blocking IP addresses on Shopify.

If you just want the click-by-click version, our step-by-step guide to blocking an IP address on Shopify covers finding the offending address and picking the right rule width. But the durable takeaway is the layer model, not the tool. Once you internalise that Liquid renders once for everybody, you stop looking for per-visitor data in it, and you start asking the better question: which layer of this request is allowed to know that, and what does it cost me to run code there?

Frequently asked questions

How do I get a visitor's IP address in Shopify Liquid?

You cannot. The Liquid request object exposes only design_mode, host, locale, origin, and page_type. There is no IP, country, user agent, or header access. Shopify storefronts render through a CDN where one Liquid render is cached and served to many visitors, so per-visitor data cannot be written into the template without either leaking it between visitors or making every page uncacheable.

What properties does the Shopify request object have?

Five: request.design_mode (true in the theme editor), request.host (the domain), request.origin (protocol plus host), request.locale (a shop_locale object), and request.page_type (the template being rendered, such as product or collection). Every one describes the URL being requested rather than the person requesting it.

Can I use Cloudflare in front of my Shopify store to block IPs?

No. Cloudflare's proxy (orange cloud) is not supported on domains connected to Shopify and produces the error "Your domain has a Cloudflare Proxy, which is not supported by Shopify". DNS records must stay DNS-only (grey cloud). Shopify's reasoning is that a proxy interferes with its ability to react to provider issues and alters request attributes before they arrive, which degrades Shopify's own bot detection.

Can a Shopify Function block a checkout based on IP address?

No. The Cart and Checkout Validation Function API is the only server-side way to validate a checkout, but its input contains cart contents, buyer journey step, buyer identity, and attributes. It has no access to the request IP, network, or user agent, so it can enforce rules about what is being bought and by whom, not about where the connection originated.

Where can I find the IP address of a Shopify order?

Order.clientIp on the GraphQL Admin API returns the IP the order was placed from, and it requires the read_orders scope. It is useful for fraud review and chargeback evidence, but it is only readable after the order exists, so it cannot be used to prevent anything.

How do I block an IP address on Shopify?

Not from Liquid, because Liquid never sees the IP. It requires a layer that does, which in practice means an app. Cordon blocks individual IPs, CIDR ranges, whole ASNs, datacenter networks, VPNs, residential proxies, Tor exits, and 200+ countries, using an app embed backed by a detection service. It decides in under 50ms, fails open so an outage cannot break checkout, and exempts verified search engines by reverse DNS. IP rules and country blocking are available on its free plan.

Is client-side IP or country detection good enough to block visitors?

Not if blocking means denying access to content. A browser-side check runs after the cached HTML has already been delivered, so the content is present in the DOM, in view-source, and readable with curl or with JavaScript disabled. Client-side detection is appropriate for personalisation such as suggesting a market or currency, but it is a curtain rather than a lock.

Start free

Ready to become a Shopify developer?

You just read how it works. Now write it yourself: real tickets from a live store, in an editor where the storefront updates as you type. Module 1 is free, no card.

Start your first lesson

Free · No credit card · Your first win in minutes

Liquidtheme-architecturesecurityShopify object model

About the author

Bas Lefeber, Founder, learnshopify.dev

Bas builds learnshopify.dev, where developers learn production-grade Shopify theme development against a live storefront. He writes about Liquid, theme architecture, and the parts of the job that still matter now that AI writes the code.

Keep going in the curriculum