Practical guide
What Apps Does a Shopify Store Use? How to Detect a Store's Tech Stack (2026)
Last reviewed: October 27, 2026
Inspect public storefront code, separate hints from evidence, and compare store snapshots with a small Python script or the enrichment Actor.
Is it Shopify? Signals you can verify yourself
Open View Source and look for Shopify.theme, Shopify.shop, or assets on cdn.shopify.com. A myshopify.com value in Shopify configuration is another hint. A linked store or copied asset alone is weaker evidence. Combine independent signals rather than deciding from a domain name.
If permitted by robots.txt, request /products.json?limit=1 and inspect whether it returns a Shopify-shaped product collection, rather than HTML or a challenge page. For WooCommerce, look for wp-content/plugins/woocommerce. Generic wp-content identifies WordPress assets, not necessarily WooCommerce. Check /wp-json/wc/store/products where available; the Actor uses the explicit versioned path /wp-json/wc/store/v1/products.
Signals can be hidden by headless storefronts, consent controls, proxies or custom themes. Empty feeds and missing globals do not rule out a platform. Detection is best effort.
Spotting apps and pixels in page source
Inspect script URLs and inline configuration, not ordinary text, comments or social profile links. The following literal examples were observed in read-only homepage GETs on October 27, 2026. They are code evidence, not proof that a service is active.
| Category | Observed fingerprint | Interpretation |
|---|---|---|
| Reviews | yotpoEnabled | Allbirds inline configuration. The observed block explicitly said disabled on that page; do not count it as an active review widget. |
| Email/SMS | window.klaviyo | Primal Kitchen inline loader/configuration for Klaviyo. |
| Subscriptions | static.rechargecdn.com | Primal Kitchen inline asset URL referencing Recharge. |
| Analytics and Google tags | www.googletagmanager.com/gtm.js | Google Tag Manager loader observed on both stores. A container does not establish which tags it fires, including GA4. |
| TikTok | analytics.tiktok.com | Primal Kitchen preconnect hint only. This is too weak to count as an active TikTok Pixel, so the script below excludes it. |
The Actor README also lists Meta Pixel, TikTok Pixel and GA4 detection, alongside review tools such as Judge.me and Okendo, email/SMS tools such as Attentive and Postscript, and Recharge subscriptions. Those are documented capabilities, not additional fingerprints verified here. A Facebook or TikTok social link is not pixel evidence. Use the Actor's code-level evidence to review these tools rather than guessing from brand mentions.
Inspect a store list with Python 3
Save this as store_stack.py and put one public homepage URL per line in stores.txt. Use final canonical URLs, including www where needed. The standard-library example checks robots, spaces requests, uses socket timeouts, caps response size and records failures separately from unknown platforms. It intentionally refuses redirects so an unchecked destination is not fetched.
import csv
import re
import sys
import time
from datetime import datetime, timezone
from html.parser import HTMLParser
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import Request, build_opener, HTTPRedirectHandler
from urllib.robotparser import RobotFileParser
UA = "StoreStackGuide/1.0 (public storefront research)"
LIMIT = 2_000_000
# Small, deliberately incomplete table of observed code signatures.
RULES = {
"Klaviyo code": r"window\.klaviyo\b",
"Yotpo configuration": r"\byotpoEnabled\s*:",
"Recharge asset reference": r"static\.rechargecdn\.com",
"Google Tag Manager loader": r"www\.googletagmanager\.com/gtm\.js",
}
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None # Inspect and approve redirects manually, including robots.
opener = build_opener(NoRedirect())
def get(url):
with opener.open(Request(url, headers={"User-Agent": UA}), timeout=15) as r:
data = r.read(LIMIT + 1)
if len(data) > LIMIT:
raise ValueError("response too large")
return data.decode(r.headers.get_content_charset() or "utf-8", "replace")
class Evidence(HTMLParser):
def __init__(self):
super().__init__()
self.parts = []
self.in_script = False
def handle_starttag(self, tag, attrs):
attrs = dict(attrs)
if tag == "script":
self.in_script = True
self.parts.append(attrs.get("src", ""))
elif tag == "link" and attrs.get("rel") not in ("preconnect", "dns-prefetch"):
self.parts.append(attrs.get("href", ""))
def handle_endtag(self, tag):
if tag == "script":
self.in_script = False
def handle_data(self, data):
if self.in_script:
self.parts.append(data)
fields = ["url", "checked_at", "status", "platform", "code_signals"]
with open(sys.argv[2], "w", newline="", encoding="utf-8") as output:
writer = csv.DictWriter(output, fieldnames=fields)
writer.writeheader()
with open(sys.argv[1], encoding="utf-8") as inputs:
for raw in inputs:
url = raw.strip()
if not url or url.startswith("#"):
continue
row = dict.fromkeys(fields, "")
row.update(url=url, checked_at=datetime.now(timezone.utc).isoformat())
try:
parts = urlsplit(url)
if (parts.scheme not in ("http", "https") or not parts.hostname
or parts.username or parts.password or parts.query or parts.fragment
or parts.path not in ("", "/")):
raise ValueError("use a public homepage URL only")
origin = parts.scheme + "://" + parts.netloc
home = origin + "/"
robots = RobotFileParser(origin + "/robots.txt")
try:
robots.parse(get(robots.url).splitlines())
except HTTPError as exc:
if exc.code != 404:
raise # Fail closed on denied, throttled or unavailable robots.
robots.parse([])
if not robots.can_fetch(UA, home):
raise ValueError("robots disallows homepage")
delay = max(2, robots.crawl_delay(UA) or 0)
rate = robots.request_rate(UA)
if rate:
delay = max(delay, rate.seconds / rate.requests)
time.sleep(delay)
parser = Evidence()
parser.feed(get(home))
evidence = "\n".join(parser.parts)
shopify = re.search(r"Shopify\.(?:shop|theme)\b|cdn\.shopify\.com", evidence, re.I)
woo = re.search(r"wp-content/plugins/woocommerce", evidence, re.I)
row["platform"] = "shopify" if shopify else "woocommerce" if woo else "unknown"
row["code_signals"] = ";".join(name for name, pattern in RULES.items()
if re.search(pattern, evidence, re.I))
row["status"] = "ok"
except Exception as exc:
row["status"] = type(exc).__name__ + ": " + str(exc)
# Keep untrusted text from becoming spreadsheet formulas.
writer.writerow({k: "'" + str(v) if str(v).startswith(("=", "+", "-", "@"))
else v for k, v in row.items()})
time.sleep(2)
python3 store_stack.py stores.txt stores.csvUse only a list of public storefronts you have reviewed. This is a local research script, not a public URL-fetching service: it does not implement DNS/IP safeguards for untrusted input. It does not execute JavaScript, probe catalog endpoints or enumerate every app. Its four code rules are deliberately conservative. Socket timeouts are not a total batch deadline. A redirect, challenge, robots failure or oversized page requires review, not a claim that apps disappeared. On 429, stop requests to that host and honor Retry-After before a later run; do not retry around blocks.
Read the public catalog for price tiers
After checking robots for each endpoint, sample Shopify /products.json?limit=1 or WooCommerce /wp-json/wc/store/v1/products?per_page=1. The Shopify collection path follows the Actor README; it is not promised by the Shopify Ajax API reference linked below. Custom storefronts may not expose it.
For Shopify, read variant price strings as decimal amounts. For WooCommerce, convert prices.price using prices.currency_minor_unit and retain prices.currency_code. Never assume a fixed currency divisor. Choose a consistent sampling unit, such as one minimum variant price per product, and record the sample size, currency, selection rule and observation time beside minimum, median and upper-tier values. A partial sample does not describe the whole catalog; currency, sale status and variant selection can change comparisons.
Start with one small page, serialize requests per host and increase collection only if allowed. Check robots and source terms, respect rate limits and Retry-After, and stop on denial or challenges. Do not bypass passwords or access controls. For pagination and comparable snapshots, see the related Shopify competitor prices and WooCommerce Store API guides.
Use cases and responsible outreach
- App and agency lead lists: qualify a reviewed store list by observed stack, theme or price tier, with evidence and timestamps.
- Competitor watch: compare consistent successful snapshots for changes in visible tools and catalog positioning.
- App churn tracking: establish a baseline, then review repeated additions and removals. Missing code can reflect consent, template or loading changes rather than cancellation.
Apply PIPEDA/CASL-style care: use public role inboxes only, avoid personal data harvesting and do not send unsolicited bulk email. A published business address is not consent. Review applicable privacy and anti-spam requirements before outreach.
The easy option: Shopify & E-Commerce Store Enrichment
Shopify & E-Commerce Store Enrichment: Tech Stack, Pixels, Contacts & App Churn is the title in the supplied README. It documents platform and theme identification, curated app and pixel code evidence, catalog price distributions, public role-based contacts and social profiles. It samples up to 250 public products and checks robots.txt while serializing requests per host.
With monitorMode: true, it keeps a deterministic domain snapshot in ecommerce-store-enrichment-state and reports app/pixel additions and removals, theme changes and median-price differences on later runs. These are observed stack changes, not verified billing events.
The README price is US$4.50 per 1,000 stores ($0.0045 per successful store), plus the apify-actor-start event fee shown in Apify pricing. The store event is charged only for status: "ok". Unreachable, HTTP-error and robots-disallowed domains cost zero in store events. Confirm the Store page before running.
For a single store, try the free Shopify Store & Tech-Stack Inspector.
Frequently asked questions
Can I see every app installed on a Shopify store?
No. Public source reveals only exposed code and assets. Backend apps, consent-gated tags and scripts loaded after interaction can be invisible. Treat results as best-effort observations, not a complete installed-app list.
Does a tracking script prove a paid subscription or active pixel?
No. A reference can be disabled, stale or gated by consent. Confirm behavior and repeat observations before interpreting a change as installation or removal.
Can I inspect WooCommerce stores without API keys?
The public WooCommerce Store API exposes published product data without merchant API keys. Availability depends on the store, its access controls and robots policy.
How much does the enrichment Actor cost?
The supplied README lists US$4.50 per 1,000 successful stores, plus the apify-actor-start event fee shown by Apify. Store events are charged only for status: ok; blocked and unreachable stores incur no store event charge.
Sources
Developer documentation was read during preparation. Literal fingerprints above were checked in public homepage HTML from Allbirds and Primal Kitchen on October 27, 2026; no installation counts or market-share estimates are inferred. Actor capabilities and pricing follow the supplied README, DEVELOPMENT.md and LIVE_TEST_RESULTS.md, rather than a fresh Actor run.
- Shopify Ajax API: theme scope, locale-aware URLs and abuse prevention (checked October 27, 2026)
- WooCommerce Store API products: public data and price fields (checked October 27, 2026)
- Allbirds public homepage: Shopify globals and inline code (checked October 27, 2026)
- Primal Kitchen public homepage: code and asset hints (checked October 27, 2026)