# EcomScout Public API — instructions for coding agents You are calling a unified ecommerce-intelligence API. It merges ClickHouse (structured store/product/change facts) and MongoDB (legacy rich store summaries: screenshots, socials, emails). You do not choose a database. Every resource is one JSON object. Do not scrape storefronts, invent SQL, or guess domains. Search first, then fetch by the domain the API returned. ## Base https://api.ecomscout.com ## Auth Send one of: - Authorization: Bearer - X-Api-Key: Do not put the key in the query string. If the server is in open-dev mode (no PUBLIC_API_KEYS), omit the key. User-issued keys share the same daily UI buckets as the app (store searches, store lookups, product searches, product/app lookups). A Starter key cannot list more stores via API than the Starter UI allows. There is also a per-key daily ceiling and a per-minute rate (X-RateLimit-* headers). On 429, wait Retry-After seconds. Docs and discovery are always open (no key, no rate limit): /, /v1/llms.txt, /v1/docs.md, /v1/openapi.json, /v1/health, /v1/examples. Human UI (no login): https://app.ecomscout.com/docs ## Error shape (always) {"error":{"code":"not_found|bad_request|unauthorized|rate_limited|query_timeout|internal","message":"...","details":...}} ## Pagination Every list: ?limit=20&offset=0 (max 100). Response: {"data":[...],"page":{"limit":N,"offset":N,"count":N,"hasMore":true,"total":N}} Stop when hasMore=false. Never invent a next page. ## Routing (pick the smallest call that answers the user) | User intent | Call | | --- | --- | | Is the API up? | GET /v1/health | | Copy-paste workflows for this API | GET /v1/examples | | Brand / URL → store | GET /v1/stores?q= | | Filtered store table, grouped like the app UI | GET /v1/stores?groupBy=country (same filters as search) | | One merchant profile | GET /v1/stores/{domain} | | Compare 2–8 merchants | GET /v1/stores/compare?domains=a.com,b.com | | Catalog / SKU search | GET /v1/products?q= (at least one filter required) | | One SKU | GET /v1/products/{domain}/{handle} | | What moved (one feed) | GET /v1/changes | | Price / new / removed SKUs | GET /v1/products/changes | | Market size by vertical | GET /v1/analytics/verticals | | Market size by country (unfiltered TAM, or one vertical) | GET /v1/analytics/countries | | Country / vertical / category breakdown of a **filtered** cohort | GET /v1/stores?groupBy=country&… | | Who is growing / shrinking | GET /v1/analytics/movers | | Coverage snapshot | GET /v1/analytics/overview | | Shopify app + install base | GET /v1/apps?q= then /v1/apps/{slug}/stores | | Track a competitor (your Signals) | POST /v1/signals {domain} then poll GET /v1/signals/{domain}/probe | | Daily units (worked: gruntstyle.com) | GET /v1/signals/gruntstyle.com/sales?from=2026-07-01 | | Ads vs SKUs (worked: magbak.com) | GET /v1/signals/magbak.com/ads/overview?days=90 | | Insights / app / product moves on a signal | GET /v1/signals/{domain}/insights , /apps , /products/changes | | Pause or delete a signal | POST /v1/signals/{domain}/stop or DELETE /v1/signals/{domain} | | Store missing from catalog | POST /v1/scrapes {domain} then poll GET /v1/scrapes/{id} | | Refresh a known store | POST /v1/scrapes {domain, force:true} | | Scrape done without polling | POST /v1/scrapes {domain, callbackUrl} — HTTPS webhook | Domain path params accept "gruntstyle.com" or "https://www.gruntstyle.com". Signal {id} also accepts the Mongo id. Signals are **per API-key owner**. Catalog routes stay global. First sale needs ≥2 inventory snapshots. Manual probe cooldown 5 minutes. ## Persona: app-builder Engineers building store look-ups, competitive catalogs, price alerts, app-stack reports, or Shopify-adjacent tools. Start: GET /v1/stores?q={domain-or-brand}&limit=5 ### store-profile — Resolve a merchant and render a profile card When: User pastes a URL, domain, or brand name and you need one canonical store object. - GET /v1/stores?q=gruntstyle&limit=5 # Fuzzy domain search. Pick the best match; do not invent a domain. - GET /v1/stores/gruntstyle.com # Unified profile: metrics, traffic mix, apps, screenshot, socials, emails. - GET /v1/stores/gruntstyle.com/products?isBestSeller=true&limit=12 # Hero SKUs for the card. ### catalog-search — Cross-store product search for a buying tool When: User searches a product idea, niche, or SKU shape across merchants. - GET /v1/products?q=dog%20toy&isBestSeller=true&priceMin=10&priceMax=80&limit=20 # Title search + price band + bestseller flag. - GET /v1/products/{domain}/{handle} # Canonical product after the user clicks a row. - GET /v1/stores/{domain} # Seller context (grade, visits, dropship flag) next to the SKU. ### price-watch — Price and assortment change feed When: You are building alerts, a watcher, or a daily digest of what moved. - GET /v1/products/changes?changeType=price_decrease&minPriceChangePct=10&since=2026-08-01&limit=50 # Meaningful markdowns only. - GET /v1/changes?entity=product&domain=gruntstyle.com&since=2026-08-01 # One store's product+store movements, already summarized. ### stack-intel — App-stack intelligence for a merchant or a Shopify app When: You need installed apps on a store, or merchants using a given app. - GET /v1/apps?q=klaviyo&limit=5 # Resolve the App Store slug. - GET /v1/apps/klaviyo-email-marketing # Catalog row + observed install-base sample. - GET /v1/apps/klaviyo-email-marketing/stores?limit=20 # Merchants observed running that app, sorted by visits. - GET /v1/stores/gruntstyle.com/apps # Full stack for one store (BuiltWith slugs × shopify_apps). ## Persona: vc-analyst Analysts at VC / PE / corp-dev desks who need TAM slices, peer sets, growth movers, and tech-stack tells — not storefront scrapes. Start: GET /v1/analytics/overview ### market-brief — One-page market brief When: Partner asks 'how big is DTC apparel' or 'what does the coverage look like'. - GET /v1/analytics/overview # Coverage counts, averages, top verticals and countries. - GET /v1/analytics/verticals?limit=30 # Store count + modeled revenue/visits/ad-spend by vertical. - GET /v1/analytics/countries?vertical=Fashion&limit=20 # Geo mix inside one vertical. ### peer-set — Build a comparable set When: You have a target domain and need peers by vertical, country, and scale. - GET /v1/stores/{domain} # Read vertical, country, visits, modeled revenue, grade, dropship flag. - GET /v1/stores?vertical=Fashion&countryCode=US&minVisits=50000&sort=revenue&limit=25 # Same-vertical, same-country, similar-scale screen. - GET /v1/stores/compare?domains=gruntstyle.com,titan.fitness,michaeltoddbeauty.com # Side-by-side cards; missing domains land in notFound, not 404. ### cohort-breakdown — Grouped store list (country / vertical trends for content) When: You need the same filtered store table as the app, rolled up by country, vertical, industry, or category — not the unfiltered TAM snapshot. - GET /v1/stores?vertical=Fashion&minVisits=10000&isDropshipper=false&groupBy=country&groupMetric=median&sort=stores&limit=25 # UI Breakdown: Fashion cohort, ≥10k visits, no dropship, by country. meta.mode=grouped. - GET /v1/stores?countryCode=US&minVisits=10000&groupBy=vertical&sort=revenue&limit=20 # Same endpoint, US cohort sliced by vertical. - GET /v1/stores?vertical=Fashion&groupBy=country&groupBy2=category&limit=50 # Nested: country → category. parent is the country; leaf is the category. - GET /v1/stores?vertical=Fashion&countryCode=US&minVisits=10000&sort=visits&limit=25 # Drop groupBy to list the stores behind one cell. ### growth-screen — Who is growing or shrinking When: You want momentum, not a static rank. - GET /v1/analytics/movers?metric=visits&direction=up&minPct=15&limit=25 # Largest recent visit jumps. - GET /v1/analytics/movers?metric=revenue&direction=down&vertical=Fashion&minPct=10 # Modeled-revenue declines inside a thesis vertical. - GET /v1/stores/{domain}/history?limit=90 # Daily snapshots for the names that survive the screen. - GET /v1/stores/{domain}/traffic?limit=24 # Monthly series + latest SimilarWeb mix (organic/paid/social). ### diligence-pack — Diligence pack for one name When: IC memo / first-look on a single merchant. - GET /v1/stores/{domain} # Metrics, conversion band, AOV, traffic mix, apps, screenshot, socials. - GET /v1/stores/{domain}/history?limit=180 # Trajectory of revenue, visits, rating, catalog size. - GET /v1/stores/{domain}/products?isBestSeller=true&limit=20 # What actually sells. - GET /v1/changes?domain={domain}&since=2026-01-01 # Assortment, price, and store-level movements in one feed. - GET /v1/stores/{domain}/apps # Stack tells (ESP, reviews, subscriptions, payments). ## Persona: competitor-tracker Agents and operators who need daily sales, inventory, ads-vs-revenue, and change alerts on stores they track — not a one-shot catalog lookup. Two observed stores: gruntstyle.com (sales chart) and magbak.com (ads lift). Start: POST /v1/signals {domain} ### start-watch — Create a signal and wait for the first probe When: User says 'track gruntstyle' or pastes a competitor URL. - POST /v1/signals {"domain":"gruntstyle.com"} # Creates the watch. Auto-tracks bestsellers. 409 if already tracked. - GET /v1/signals/gruntstyle.com/probe # Poll until progress.status is done or failed. First sale needs two snapshots. - GET /v1/signals/gruntstyle.com/stats # After two probes: ~$10.8k / 360 units / 19 days on Grunt Style (Aug 2026 observe). ### sales-brief — Daily units on an apparel rival When: User wants yesterday's sales, a chart, or which size sold. - GET /v1/signals/gruntstyle.com/sales?from=2026-07-01 # Peak 2 Jul = 64 units / $1,919. Hero SKU american-reaper-2-0. - GET /v1/signals/gruntstyle.com/inventory?days=90 # Restocks + start/end stock per variant. - GET /v1/signals/gruntstyle.com/insights?kind=sales_spike # Pre-built spike / restock cards if you do not want to compute them. ### ads-impact — Which ads moved which SKUs (MagBak) When: User asks which ads moved sales, lift, or spend vs revenue. Prefer magbak.com over gruntstyle.com for this. - POST /v1/signals {"domain":"magbak.com"} # Watch MagBak if not already tracked. - GET /v1/signals/magbak.com/ads/overview?days=90 # iPhone 15 MagBak +82.7% lift on high-ad days; 729 Meta + 410 Google ads. One call. - GET /v1/signals/magbak.com/sales?from=2026-07-01 # $1.24M / 20k units / 51 days. Peak 5 Jul = $33.1k. - GET /v1/signals/magbak.com/ads # Paginated creatives if you need the library itself. - POST /v1/signals/magbak.com/ads/probe # Refresh Meta/TikTok/Google when events look stale. ### changes-insights — What changed on a tracked store When: Daily briefing: sales spikes, app installs, catalog moves. - GET /v1/signals/magbak.com/insights # sales_spike, restock, ad_new_campaign, app_change, … - GET /v1/signals/magbak.com/apps/changes # Install / uninstall history. - GET /v1/signals/magbak.com/products/changes # Price / new / removed SKUs on this watch. - GET /v1/signals/insights # Account-wide inbox across every watch on this key. ### lifecycle — Pause, edit, or delete a watch When: User is done tracking, or wants to swap SKUs. - PATCH /v1/signals/gruntstyle.com {"status":"paused"} # Stop cron probes without deleting history. - POST /v1/signals/gruntstyle.com/stop # Same as paused. Alias for agents. - DELETE /v1/signals/gruntstyle.com # Remove the watch. Domain CH data stays for other users. ## Persona: store-scraper Agents that hit a 404 on GET /v1/stores/{domain} and need the same on-demand scrape as Request a store — poll or webhook. Start: POST /v1/scrapes {domain} ### queue-poll — Queue a scrape and wait When: Search returned nothing. User pasted a URL that is not in the catalog. - GET /v1/stores?q=newbrand.com&limit=5 # Confirm it is missing before you spend a scrape credit. - POST /v1/scrapes {"domain":"newbrand.com"} # Queues the job. 202 + id. already_exists if we already have it. - GET /v1/scrapes/{id} # Poll queued → validating → scraping → done|failed. TTL 15 min. - GET /v1/stores/newbrand.com # Read the profile after done. ### callback — Webhook instead of poll When: Your agent cannot sit in a loop. You have an HTTPS hook. - POST /v1/scrapes {"domain":"newbrand.com","callbackUrl":"https://hooks.example.com/ecomscout"} # We POST scrape.done or scrape.failed once. HTTPS only. ### refresh-bulk — Force refresh or bulk ingest When: Store exists but looks stale, or the user pasted a list. - POST /v1/scrapes {"domain":"gruntstyle.com","force":true} # Re-run even if already_exists. 5 min cooldown. - POST /v1/scrapes/bulk {"urls":["a.com","b.com"]} # Max 10. Same scrapeRequests cap as the app. ## Field notes - Money is USD unless a product sets currency. - GET /v1/stores without groupBy returns store cards. With groupBy it returns grouped rows (meta.mode=grouped). Do not treat grouped rows as stores. - monthlyRevenueUsd is {low,high} on a store card. On a grouped row, metrics.monthlyRevenueUsd is one aggregated number from the high band (mean/median/max/min). Label it as modeled, not GMV. - monthlyVisits and SimilarWeb fields are modeled/third-party estimates, not first-party analytics. - isDropshipper is a classifier, not a legal fact. Always say "classified as". - ecomScoutRating is 0–100; ecomScoutGrade is the letter bucket. Traffic is one input, not a hard fail. - sources[] on a store lists backends that contributed (clickhouse:store_snapshots, mongo:stores). - Empty arrays / zeros mean "no observation", not an error. Missing SimilarWeb → traffic.latest is null. - change feeds are observation diffs between scrapes, not a complete commerce event stream. ## Do not - Do not invent SQL or hit ClickHouse/Mongo yourself. - Do not scrape storefronts when this API already has the field. - Do not page past hasMore=false. - Do not treat modeled revenue as audited financials in a memo. - Do not collapse {low,high} to a midpoint without labeling it as a midpoint of a model. - Do not scrape a storefront yourself — POST /v1/scrapes (missing store) or POST /v1/signals (watch). - Do not scrape a storefront to track a competitor — POST /v1/signals and poll probe. - Do not read another user's signals. {domain} is scoped to the key's clerkId. Partner service keys with signals:read may GET a domain already on Signals; they cannot create or alter watches. ## More - Human UI (playground + reference, no login): https://app.ecomscout.com/docs - Human + code samples: https://api.ecomscout.com/v1/docs.md - Machine recipes: https://api.ecomscout.com/v1/examples - OpenAPI 3.1: https://api.ecomscout.com/v1/openapi.json