Shopify app detector: how to see which apps a store uses, and how to build one
Shopify offers no public API to list the apps another store has installed; every "app detector" reads the storefront, where each app leaves footprints that vanish when it is uninstalled. Source: Arcadium Data, data as of .
Developers of Shopify apps want leads: stores that run a competitor, or none. Agencies want to see what a prospect has installed. Investors track which apps gain share. All of them end up at the same question: given a domain, which apps does the store run?
The Shopify Admin API answers it only for your own store, with your own token. Everything else is storefront analysis. This guide shows what is public, which footprints can be trusted, how detectors produce false positives, and how to get the answer without building the dictionary yourself.
The storefront route
1. Is it a Shopify store?
Fetch the home page and look for Shopify.theme in the inline script, a cdn.shopify.com asset, or the X-ShopId response header. Cheaper and more precise: every Shopify storefront answers /meta.json with the shop's name, description, country, currency, myshopify_domain and published product count. Headless stores (Next.js on the custom domain, for example) often fail on the custom domain but still answer on their *.myshopify.com address, so try both.
curl
# Store identity and size (public, no key)
curl -s https://kith.com/meta.json
# Newest products first: ?limit=1 is the freshness signal
curl -s "https://kith.com/products.json?limit=1"
# Does the home page expose the theme object?
curl -s https://kith.com/ | grep -o 'Shopify.theme = {[^}]*}' | head -1
Shopify's default robots.txt does not mention /meta.json, /products.json or /.well-known/ucp (the endpoint through which Shopify advertises its agent-commerce interface), and it allows the home page; it disallows cart, checkout, account and search paths. Check the store's own robots.txt, as stores can edit it, and keep to a pace the host tolerates: on one IP we saw about seven back-to-back requests to the same store before a 429, and Shopify also answers 430 ("Security Rejection") to flagged clients. Different stores do not share that limit.
2. Footprints, in order of trust
- Theme app extensions. Apps built for online store 2.0 load assets from
cdn.shopify.com/extensions/<uuid>/<handle>-<version>/assets/<file>. The handle is usually the app's name (judgeme-819,pagefly-ai-page-builder-379). Shopify removes the files on uninstall, so presence means installed. - App blocks and embeds. Rendered app blocks are wrapped in comments:
<!-- BEGIN app block: shopify://apps/<app-handle>/blocks/<block>/<uuid> -->. The app handle is unique per app and Shopify stops rendering the block on uninstall. This is the best footprint, but the handle is not the App Store slug. - Script tags. Older apps register a script tag, which Shopify writes into the head as a list of URLs with escaped slashes (
https:\/\/cdn.example.com\/app.js?shop=...). Unescape\/before you match. Also removed on uninstall. - Vendor hosts elsewhere (inline config,
<script src>, preconnect hints). - CSS class names (
jdgm-for Judge.me). They can outlive an uninstall: treat as a hint only.
Python
import re
import requests
EXT = re.compile(r"cdn\.shopify\.com/extensions/[0-9a-f-]{36}/([a-z0-9-]+?)(?:-\d+)?/assets/")
BLOCK = re.compile(r"<!-- BEGIN app block: shopify://apps/([a-z0-9-]+)/blocks/")
SCRIPT_TAGS = re.compile(r'var urls = \[(.*?)\];', re.S)
def fingerprints(domain: str):
r = requests.get("https://" + domain + "/", timeout=30, headers={"User-Agent": "your-bot/1.0 (+https://example.com/bot)"})
r.raise_for_status()
html = r.text # read the whole page: app embeds sit near </body>
found = {"extensions": sorted(set(EXT.findall(html))), "blocks": sorted(set(BLOCK.findall(html)))}
m = SCRIPT_TAGS.search(html)
found["script_tags"] = re.findall(r'"(https?:[^"]+)"', m.group(1).replace("\\/", "/")) if m else []
return found
print(fingerprints("kith.com"))
TypeScript
const EXT = /cdn\.shopify\.com\/extensions\/[0-9a-f-]{36}\/([a-z0-9-]+?)(?:-\d+)?\/assets\//g;
const BLOCK = /<!-- BEGIN app block: shopify:\/\/apps\/([a-z0-9-]+)\/blocks\//g;
export async function appFootprints(domain: string) {
const res = await fetch('https://' + domain + '/', { headers: { 'User-Agent': 'your-bot/1.0 (+https://example.com/bot)' } });
const html = await res.text(); // whole page, not the first 100 KB
const uniq = (re: RegExp) => [...new Set([...html.matchAll(re)].map((m) => m[1]))];
return { extensions: uniq(EXT), blocks: uniq(BLOCK) };
}
3. Map handles to apps
A handle is not a product name. You need a dictionary: footprint to app, with the App Store listing as the reference. Listing pages on apps.shopify.com carry SoftwareApplication JSON-LD (name, brand, rating, review count) and the plan prices; build the dictionary from the listings and from the handles you see. It is a long tail: in two October 2026 samples of our own, roughly 30% of the extension handles found were not yet in the dictionary, and each new sample brings more.
How detectors get it wrong
- Shared vendor hosts. One vendor serves several apps from one host (
cdn.secomapp.comserves a promotion popup and an affiliate app). Match on host plus path prefix, not on host. - Hosts inside another app's code. A restock app that references the Klaviyo API host does not mean Klaviyo is installed. A host that appears inside another known app's block never counts.
- Links are not installs. Footer links to Trustpilot or a policy generator are not apps. Skip URLs inside
<a>tags and match asset subdomains, not marketing domains. - Build tags in extension directories (
2026-10-22-17-01-59-UTC-,version_d767d6f1-...) change with each deploy. Match by app handle or by a vendor-specific asset file instead. - Generic handles (
back-in-stock,product-options) are used by several vendors. Use a distinctive asset or the app block. - Truncated pages. App embeds sit near the closing body tag. Capping the download at 600 KB lost about 5% of detections in our tests, 1,000 KB about 3%; the average home page is about 0.4 MB.
- Churn needs guards. The first scan of a store is a baseline, not a wave of installs. Count an uninstall only when the whole page was read, and version your dictionary so that a new fingerprint does not look like a new install.
What to keep out
Store names that read like a person's name (many small stores are sole traders) are personal data in a directory. Descriptions can contain emails and phone numbers. Keep company-level facts only.
The fast route: store profile and apps by domain
Arcadium Data runs a Shopify store directory (discovered through public web archives and validated store by store) and scans stores for apps. Two routes answer from it, both open to anonymous point lookups.
Calls on this page work without a key from one IP (10 requests a minute, 100 a day, short answers, point lookups). A free key lifts the limits: sign up. Routes, parameters and fields: OpenAPI.
curl
curl -s "https://api.arcadiumdata.io/v1/shops/kith.com"
Response (abridged)
{
"domain": "kith.com",
"myshopifyDomain": "kithnyc.myshopify.com",
"name": "Kith",
"country": "US",
"currency": "USD",
"shipsToCount": 197,
"productCount": 25001,
"productCountCapped": true,
"status": "open",
"theme": "SEED",
"headless": false,
"ucpVersion": "2026-08-25",
"latestProductPublishedAt": "2026-10-22T19:26:25.000Z",
"checkedAt": "2026-10-23T00:35:37.752Z",
"activeApps": 0
}
productCount stops at 25,001 (productCountCapped), because Shopify's public counter does. status distinguishes open stores from password-protected and closed ones. For a domain that is not a Shopify store, or not in our directory, the route answers 404 not_found.
The apps of one store, with the install and uninstall events found between daily scans:
curl
curl -s "https://api.arcadiumdata.io/v1/shops/kith.com/apps"
Response (abridged)
{
"domain": "kith.com",
"myshopifyDomain": "kithnyc.myshopify.com",
"count": 0,
"data": [],
"events": []
}
Each element of data has app (our slug, for example klaviyo), confidence, installed, firstSeen, lastSeen and removedOn; each element of events has app, event and day. The call above returned an empty list when we ran it on 2026-10-25: the app scan covers stores in a daily slice, so an empty list means "no app detected so far", not "no apps". Check checkedAt on the profile and call again later.
Python
import requests
def shop_apps(domain: str):
base = "https://api.arcadiumdata.io/v1/shops/" + domain
profile = requests.get(base, timeout=30)
if profile.status_code == 404:
return None # not a Shopify store we know
profile.raise_for_status()
apps = requests.get(base + "/apps", timeout=30).json()
return {
"store": profile.json()["name"],
"status": profile.json()["status"],
"apps": [a["app"] for a in apps["data"] if a["installed"]],
"events": apps["events"],
}
print(shop_apps("kith.com"))
TypeScript
type Apps = { count: number; data: { app: string; installed: boolean; firstSeen: string }[]; events: { app: string; event: string; day: string }[] };
const res = await fetch('https://api.arcadiumdata.io/v1/shops/kith.com/apps');
if (res.ok) {
const body = (await res.json()) as Apps;
console.log(body.count, body.data.map((a) => a.app));
}
For a company page that combines the Shopify profile with hiring, technology named in job ads and registry ids, call GET /v1/companies/{domain}: the shop field holds the store profile. The shop directory is company-level: stores whose name reads like a private person are left out.
In a random sample of 500 open stores that we scanned in October 2026, 69% ran at least one app we map, with 2.2 apps per store on average. The most common were Judge.me (18.4%), Klaviyo (17.8%), Instafeed (12.4%), Mailchimp (8.6%), Shopify Forms (5.8%) and PageFly (3.8%). That is a sample from our directory, not the whole platform, and the long tail of unmapped apps is large.
Comparison
| Build it yourself | Chrome-extension detectors | Arcadium Data /v1/shops | |
|---|---|---|---|
| Scale | As large as your crawl budget | One store at a time, by hand | Point lookups by domain; bulk through a key or licence |
| Dictionary of apps | You build and maintain it | Vendor-maintained | Maintained, versioned; unmapped handles tracked |
| Installs and uninstalls over time | You diff scans and guard against false waves | No history | events with dates |
| Precision controls | Yours | Not documented | Host plus path matching, app blocks, version gates |
| Traffic to the store | Yours (rate limits apply) | From your browser | None from you |
| Store profile (theme, country, products) | /meta.json and /products.json | Partial | /v1/shops/{domain} |
| Coverage | What you scan | Any store you visit | Stores in our directory; daily slice for apps |
Get the data without the plumbing
Arcadium Data serves these datasets from one REST API and one MCP server, with sources, licences and dates on every answer. No key is needed to try the routes in this guide.
Related
Sources
Last updated 2026-10-25. This guide is ours and independent of the organisations it describes. Found a mistake? Tell us and we correct it.
Questions
Can I see which apps another Shopify store has installed through an API?
Not through Shopify. The Admin API shows only your own store with your own token. Other stores can only be analysed from their public storefront, where apps leave footprints such as theme app extension files, app blocks and script tags.
How does a Shopify app detector work?
It loads the home page and matches known footprints (extension asset paths, app block comments, script tag URLs, vendor hosts) against a dictionary of apps. The quality depends on the dictionary and on rules that avoid shared hosts and links.
Does an uninstalled app disappear from the footprints?
Theme app extension files, app blocks and script tags are removed when the app is uninstalled. Class names and leftover code may stay, so they are not counted as installs.
Is it allowed to read /meta.json and /products.json?
They are public endpoints that Shopify’s default robots.txt does not disallow. Stores can change their robots.txt, so check it, identify your bot and keep the request rate low; Shopify rate-limits repeated requests to one store.
Why does the apps route return an empty list for a Shopify store?
The app scan reads stores in a daily slice, so a store may not be covered yet. An empty list means no app has been detected so far. Compare checkedAt on the shop profile and retry later.
Cite as: Arcadium Data, "Shopify app detector: how to see which apps a store uses, and how to build one", https://arcadiumdata.io/guides/shopify-app-detector-store-apps/, data as of 2026-10-25.