REST API REFERENCE

UNDERCART API

Query enriched Shopify store data — revenue estimates, technology stacks, ad presence, and more — over HTTPS with a single API-key header. Every response is JSON.

Pointing an AI agent at this? Skip the HTTP — add Undercart as an MCP connector and ask in natural language.MCP docs →

Overview

The Undercart REST API is served under the base URL below. All endpoints require authentication and return a consistent JSON envelope. Endpoints that expose a store's data count against your plan's monthly store lookups, and listing or searching stores against its monthly searches.

Base URL:  https://undercart.co/api/v1

Authentication

Authenticate every request with an API key in the X-API-Key header. Create and manage keys from the dashboard (Settings → API keys); the full key is shown once at creation and stored only as a hash. Keys look like sk_live_…. A logged-in browser session cookie also authenticates requests to the same endpoints.

curl https://undercart.co/api/v1/me \
  -H "X-API-Key: sk_live_9f2a…"

Response format

Every endpoint returns the same envelope. On success, data holds the payload; on failure, error holds a human-readable message. List endpoints add a meta object with pagination details.

{
  "success": true,
  "data": { /* endpoint payload */ },
  "meta": {                 // list endpoints only
    "limit": 50,
    "total": 681240,
    "totalIsApproximate": true,   // true for the unfiltered catalog count
    "totalIsLowerBound": false,   // true when a filtered count stopped at its cap
    "nextCursor": "eyJ2Ijo…",     // pass back as ?cursor= for the next page
    "prevCursor": null
  }
}

Counts are bounded so a broad filter never holds up the response: the unfiltered catalog is an estimate (totalIsApproximate), and a filtered count stops at 1,000 — when totalIsLowerBound is true, read total as “1,000+”.

List endpoints use keyset (cursor) pagination: read meta.nextCursor and pass it back as ?cursor= with dir=next (or prev). A null cursor means there are no more pages in that direction.

Errors & rate limits

Errors use standard HTTP status codes with success: false and an error message.

ParameterTypeDescription
400Bad RequestInvalid or missing query/body parameters.
401UnauthorizedMissing or invalid API key / session.
403ForbiddenAuthenticated but lacking permission (admin-only route).
404Not FoundNo matching store.
429Too Many RequestsMonthly store-lookup or search allowance (or, for export, the monthly export-row allowance) reached; the message includes the plan cap, the reset date and the upgrade link.
500Server ErrorUnexpected error.

Endpoints that return one store's data (by domain, by ID, lookup, tech stack, revenue history) charge a store lookup once per distinct store per calendar month (UTC): asking again for a store you already looked up this month is free, whichever endpoint you use. Listing and searching stores (GET /api/v1/stores) charges a search per request, every page included; repeating an identical request that month is free. On the free plan (25 store lookups and 50 searches a month), a lookup or a search made with an API key uses 5 of them, where the same thing in the web app uses one; Unlimited ($29/mo) has no caps. Bulk export (paid plans) uses neither; it has its own monthly row allowance (see Bulk export). Past a cap, the endpoint returns 429 until the start of the next month; the body carries a machine-readable quota object (used, cap, tier, resetsAt, plus meter, "stores" or "searches", and the credits the request needed).

List / search stores

GET/api/v1/stores

Search and filter the store catalog. Returns a page of store summaries. By default the full catalog is in scope (including dormant, not-yet-enriched discovery rows); pass enriched=true to restrict to fully enriched stores.

ParameterTypeDescription
qstringCase-insensitive match on domain or store name. A pasted URL or hostname is reduced to the bare domain first.
techStackstringComma-separated app/tech names (case-insensitive, e.g. klaviyo,gorgias); matches stores running any of them. Call once per app to require several.
sortenumestimatedAnnualRevenue | productCount | lastCrawledAt | firstSeenAt | name | domain (default: estimatedAnnualRevenue — stores not yet analyzed rank by their preliminary estimate).
orderenumasc | desc (default: desc).
cursor / dirstringKeyset pagination cursor and direction (next | prev).
limitnumberPage size, 1–100 (default: 50).

Every filter from the Store Explorer's “Add filter” menu, under the same name as in the explorer's URLs. All filters AND together.

ParameterTypeDescription
descriptionstringDescription keywords. Matches the store's own description. Every keyword must appear, in any order. Use "quotes" for a phrase.
countrystringCountry. An ISO country code, e.g. "US".
categorystringCategory. Exact stored value, e.g. "Beauty & Cosmetics".
currencystringCurrency. Exact stored value, e.g. "USD". The store's own checkout currency, from /meta.json.
revenueMin / revenueMaxnumberEst. annual revenue bounds, inclusive (USD). Stores we haven't analyzed yet are included by their popularity-based estimate.
monthlyRevenueMin / monthlyRevenueMaxnumberEst. monthly revenue bounds, inclusive (USD). The same estimate as annual revenue, as a monthly run-rate. Stores we haven't analyzed yet are included by their popularity-based estimate.
productsMin / productsMaxnumberProduct count bounds, inclusive (count). Only known for stores we've researched in depth — a few thousand of the ~700k in the catalog.
priceMin / priceMaxnumberAvg product price bounds, inclusive (USD). Only known for stores we've researched in depth — a few thousand of the ~700k in the catalog.
reviewsMin / reviewsMaxnumberTotal reviews bounds, inclusive (count). Only known for stores we've researched in depth — a few thousand of the ~700k in the catalog.
reviewRateMin / reviewRateMaxnumberReview velocity bounds, inclusive (rate). Only known for stores we've researched in depth — a few thousand of the ~700k in the catalog. Observed new reviews per day between the last two comparable crawls.
onSaleMin / onSaleMaxnumberCatalog on sale bounds, inclusive (percent, 0–100). Only known for stores we've researched in depth — a few thousand of the ~700k in the catalog. Share of the catalog currently discounted.
themestringTheme. Exact stored value, e.g. "Dawn". The base Theme Store theme, even where the store renamed its copy.
hiringbooleantrue — only stores matching "Hiring now". Only known for stores we've researched in depth — a few thousand of the ~700k in the catalog. Has a careers page — the cheapest proxy for a team that is growing.
localesMin / localesMaxnumberLanguages bounds, inclusive (count). Distinct languages the store publishes its pages in (hreflang) — international expansion.
rankMin / rankMaxnumberTraffic rank bounds, inclusive (rank; 1 = most popular). Popularity rank blending Tranco with HTTP Archive/CrUX. Lower is more popular; stores in neither are unranked.
trafficTrendstringTraffic trend. One of: rising (Rising), steady (Steady), falling (Falling). Chrome UX Report: the last 3 months' worldwide rank against the same months a year earlier. Only a whole-bucket move (e.g. Top 50k → Top 10k) counts.
topMarketstringTop market. An ISO country code, e.g. "US". One of the store's top 3 countries for Chrome traffic, relative to each country's size. Where its visitors are, not where it is based.
emailsMin / emailsMaxnumberEmails per week bounds, inclusive (rate). Only known for stores we've researched in depth — a few thousand of the ~700k in the catalog. Marketing emails we captured per week, averaged since we subscribed or over the last 30 days, whichever is shorter.
enrichedbooleantrue — only stores matching "Fully enriched". Only stores we've crawled ourselves, with revenue, ads, social and catalog data. The rest carry just what HTTP Archive saw: tech stack, description, country and traffic rank.
trackingEmailbooleantrue — only stores matching "We capture their email". Only known for stores we've researched in depth — a few thousand of the ~700k in the catalog. Subscribed to their newsletter AND receiving mail — email intelligence is flowing.
curl https://undercart.co/api/v1/stores \
  -H "X-API-Key: sk_live_9f2a…" \
  -G -d "revenueMin=1000000" \
     -d "techStack=klaviyo" \
     -d "sort=estimatedAnnualRevenue" \
     -d "limit=2"

# → 200 OK
{
  "success": true,
  "data": [
    {
      "id": "clw…",
      "domain": "maplestone.co",
      "name": "Maple & Stone",
      "country": "US",
      "category": "Home & Living",
      "estimatedAnnualRevenue": 11400000,
      "estimatedMonthlyRevenue": 950000,
      "revenueConfidence": 0.72,
      "revenueTier": "high",
      "revenueBand": null,
      "categoryGuessed": false,
      "productCount": 1284,
      "lastCrawledAt": "2026-06-29T02:00:00.000Z",
      "techCount": 37,
      "topTech": ["Shopify Plus", "Klaviyo", "Gorgias", "Recharge"]
    }
  ],
  "meta": { "limit": 2, "total": 4120, "nextCursor": "eyJ2Ijo…" }
}

The list includes stores we haven't analyzed yet. For those, estimatedAnnualRevenue is null and revenueBandcarries a preliminary annual range estimated from the site's popularity (e.g. "$100K–$1M"); it is null once a full estimate exists. categoryGuessed is truewhen the category was inferred from the store's homepage (description and title) rather than from analyzing the store.

Get store by domain

GET/api/v1/stores/by-domain?domain=<domain>

Returns the full enriched profile for a single known store, keyed by domain — revenue, plan, catalog stats, active tech stack, ad presence, social profiles, and traffic (rank, year-on-year trend and top markets from the Chrome UX Report). A URL or www.host is accepted, and a hostname merged into another store (a duplicate listing, or a brand's account./checkout. host) answers with that store. Responds 404 if the domain is not in the catalog.

curl "https://undercart.co/api/v1/stores/by-domain?domain=maplestone.co" \
  -H "X-API-Key: sk_live_9f2a…"

# → 200 OK
{
  "success": true,
  "data": {
    "id": "clw…",
    "domain": "maplestone.co",
    "name": "Maple & Stone",
    "category": "Home & Living",
    "country": "US",
    "foundedYear": 2019,
    "shopifyPlan": "Shopify Plus",
    "estimatedAnnualRevenue": 11400000,
    "revenueConfidence": 0.72,
    "revenueBand": null,
    "redirectsToDomain": null,
    "productCount": 1284,
    "avgProductPrice": 68.5,
    "currency": "USD",
    "trafficRank": "#48,210",
    "trafficTrend": "rising",
    "trafficTrendDetail": "Top 50K in Chrome traffic, up from Top 100K a year earlier",
    "topMarkets": {
      "month": "Aug 2026",
      "markets": [{ "country": "CA", "rank": "Top 10K" }]
    },
    "techStack": [
      { "techName": "Klaviyo", "techCategory": "Email & SMS" }
    ],
    "adPresence": [
      { "platform": "meta", "isActive": true, "lastCheckedAt": "2026-06-29T…" }
    ],
    "socialProfiles": [
      { "platform": "instagram", "profileUrl": "https://…", "followers": 84000 }
    ]
  }
}

A store with no published estimate — not yet analyzed, no product catalog found, or judged not to be a store — has a null estimatedAnnualRevenue and a revenueBand (a preliminary annual range from its popularity) instead. redirectsToDomainis set when the store's homepage now lands on another site: its data was read there, so look that domain up instead. trafficRank is a position ("#48,210") or, where only a Chrome UX Report bucket is known, the bucket ("Top 50K"); trafficTrend is rising, steady, falling or null.

Look up or enqueue a crawl

POST/api/v1/stores/lookup

Look up a store by domain. If it is already known, the full profile is returned immediately (200). If it is not yet in the catalog, a discovery/enrichment job is enqueued and the endpoint responds 202 Accepted with a job id.

curl -X POST https://undercart.co/api/v1/stores/lookup \
  -H "X-API-Key: sk_live_9f2a…" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "newstore.com" }'

# → 202 Accepted   (store was not yet known)
{
  "success": true,
  "data": { "jobId": "123", "domain": "newstore.com", "status": "queued" }
}

Get store by ID

GET/api/v1/stores/{storeId}

Returns the complete store record by its Undercart ID, including the full tech stack, recent revenue history, ad presence, social profiles, and any traffic data. Like the other store endpoints it carries revenueBand when there is no published estimate; POST /stores/lookup returns the same record for a known store.

Get store tech stack

GET/api/v1/stores/{storeId}/tech

Returns the store's detected technologies grouped by category (email/SMS, support, subscriptions, analytics, etc.), each with version, first/last-seen timestamps, and whether it is currently active. Metered like the other single-store endpoints.

Get revenue history

GET/api/v1/stores/{storeId}/revenue

Returns the store's historical revenue estimates, most recent first. Each row's method names the signals it blended, e.g. composite(traffic,product,social), with a suffix when our revenue check corrected it. While a store publishes no estimate (see revenueBand) its history is empty.

ParameterTypeDescription
limitnumberNumber of estimates to return, 1–100 (default: 30).
methodenumRows whose method starts with: composite | traffic | tech_stack | product | ad_spend | social. Current estimates are all composite; the rest are legacy single-signal rows.

Bulk export

GET/api/v1/stores/export

Streams a filtered result set for offline use. Accepts every filter the list endpoint does, plus sort and order, with two differences in naming: the domain/name search is search (not q), and technologies are repeated tech (running now), techAdded and techRemoved params (changed within the last days, default 30, max 365) instead of techStack. Choose the output with format=csv (default) or format=json, and pick columns with a comma-separated columns= list from: domain, name, category, country, currency, shopifyPlan, estimatedAnnualRevenue, estimatedMonthlyRevenue, revenueTier, revenueConfidence, productCount, avgProductPrice, totalReviews, foundedYear, isActive, lastCrawledAt, description, revenueBand, trafficRank, trafficTrend, revenue. revenueBand is the preliminary range for stores without a published estimate (their revenue cells are empty). lastCrawledAt is admin-only and is omitted for everyone else, whether or not it is named. revenue is the estimate where one is published and the preliminary range otherwise. Set how many rows with limit (the top N in the chosen sort; default and maximum 1,000). Export needs the paid plan, which includes 3,000 rows a month; past that the endpoint returns 429until the start of the next month (UTC), and an export that would cross it is cut short at what's left.

curl "https://undercart.co/api/v1/stores/export?format=json&limit=500&revenueMin=1000000" \
  -H "X-API-Key: sk_live_9f2a…" -o stores.json

Identity

GET/api/v1/me

Returns the authenticated caller's id and role. Useful for verifying a key works.

{ "success": true, "data": { "id": "usr_…", "role": "user" } }
GET AN API KEY
Start free with 25 store lookups/month. Provision a key from the dashboard.
Start free →