Developer guides

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

  1. 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.
  2. 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.
  3. 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.
  4. Vendor hosts elsewhere (inline config, <script src>, preconnect hints).
  5. 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

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 yourselfChrome-extension detectorsArcadium Data /v1/shops
ScaleAs large as your crawl budgetOne store at a time, by handPoint lookups by domain; bulk through a key or licence
Dictionary of appsYou build and maintain itVendor-maintainedMaintained, versioned; unmapped handles tracked
Installs and uninstalls over timeYou diff scans and guard against false wavesNo historyevents with dates
Precision controlsYoursNot documentedHost plus path matching, app blocks, version gates
Traffic to the storeYours (rate limits apply)From your browserNone from you
Store profile (theme, country, products)/meta.json and /products.jsonPartial/v1/shops/{domain}
CoverageWhat you scanAny store you visitStores 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.

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.