EcomScout Stores API

Merchant search, profile, compare, contacts. Subresources: traffic, products, apps, history, changes, contacts.

Operations 11

GET /v1/stores Search stores, or group the same cohort (UI breakdown) #
GET /v1/stores/compare Side-by-side store cards (2–8 domains) #
GET /v1/stores/{domain} Unified store profile #
GET /v1/stores/{domain}/history Daily store snapshots #
GET /v1/stores/{domain}/traffic Traffic series + latest SimilarWeb #
GET /v1/stores/{domain}/competitors Ranked competitor stores (same list as the app's Competitors tab) #
GET /v1/stores/{domain}/inventory-probe Stock-readability verdict + per-product tags #
GET /v1/stores/{domain}/contacts Company info, emails, phones, LinkedIn #
GET /v1/stores/{domain}/verify-contacts Status of a contact verification run #
POST /v1/stores/{domain}/verify-contacts Find & verify contacts for one store #
GET /v1/contacts Contacts by domain query (alias) #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/ecomscout-com:ecomscout-com-stores-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

ecomscout-com-stores-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: EcomScout Public Stores API
  version: 0.7.0
  description: Unified ecommerce intelligence API for coding agents, app builders, and VC/PE analysts. Stores, products, apps, analytics, changes, signals, scrapes — one JSON object per resource. Read /v1/llms.txt then /v1/examples before calling data endpoints. Only 2xx responses burn daily quota and plan buckets; 4xx do not. Store contacts (company, emails, phones) on GET /v1/stores/{domain} and GET /v1/stores/{domain}/contacts.
servers:
- url: https://api.ecomscout.com
security:
- apiKey: []
- bearer: []
tags:
- name: Stores
  description: 'Merchant search, profile, compare, contacts. Subresources: traffic, products, apps, history, changes, contacts.'
paths:
  /v1/stores:
    get:
      tags:
      - Stores
      summary: Search stores, or group the same cohort (UI breakdown)
      description: 'Without groupBy: paginated store cards. With groupBy=country|vertical|industry|category: same filters, transformed into a grouped table (app store-list Breakdown). Use this — not /v1/analytics/countries — when the cohort is filtered (minVisits, includeApps, includeProducts, country regions, isDropshipper, revenue/rating bands). meta.mode is list or grouped.'
      operationId: listStores
      parameters:
      - name: q
        in: query
        schema:
          type: string
        description: Domain substring
      - name: vertical
        in: query
        schema:
          type: string
      - name: category
        in: query
        schema:
          type: string
      - name: industry
        in: query
        schema:
          type: string
      - name: country
        in: query
        schema:
          type: string
        description: Country names, ISO codes, or regions (Europe, EU, Nordics, DACH, Baltics, Benelux). Comma list. Overrides countryCode.
      - name: countryCode
        in: query
        schema:
          type: string
          minLength: 2
          maxLength: 2
        description: ISO-2 alias for country when country is omitted.
      - name: excludeCountries
        in: query
        schema:
          type: string
        description: Omit these countries / regions.
      - name: excludeVerticals
        in: query
        schema:
          type: string
      - name: isDropshipper
        in: query
        schema:
          type: boolean
      - name: appSlug
        in: query
        schema:
          type: string
        description: Single app. Alias for includeApps with one slug.
      - name: includeApps
        in: query
        schema:
          type: string
        description: Comma slugs, OR unless appsMatch=all.
      - name: requireApps
        in: query
        schema:
          type: string
        description: Comma slugs, AND (must have all).
      - name: excludeApps
        in: query
        schema:
          type: string
      - name: appsMatch
        in: query
        schema:
          type: string
          enum:
          - any
          - all
      - name: removedApps
        in: query
        schema:
          type: string
        description: Comma slugs. Stores that uninstalled any of these inside the window (default last 30 days; appRemovedSince to widen, max 180 days). Different from excludeApps.
      - name: installedApps
        in: query
        schema:
          type: string
        description: Comma slugs. Stores that newly installed any of these inside the window (appInstalledSince to widen).
      - name: appRemovedSince
        in: query
        schema:
          type: string
          format: date
      - name: appInstalledSince
        in: query
        schema:
          type: string
          format: date
      - name: includeProducts
        in: query
        schema:
          type: string
        description: 'Pipe tokens: cat:Name|sub:Dog|type:Dog Beds|name:cooling mat'
      - name: excludeProducts
        in: query
        schema:
          type: string
      - name: productMatch
        in: query
        schema:
          type: string
          enum:
          - any
          - all
      - name: minVisits
        in: query
        schema:
          type: integer
      - name: maxVisits
        in: query
        schema:
          type: integer
      - name: minRevenue
        in: query
        schema:
          type: number
      - name: maxRevenue
        in: query
        schema:
          type: number
      - name: minRating
        in: query
        schema:
          type: number
      - name: groupBy
        in: query
        schema:
          type: string
          enum:
          - country
          - vertical
          - industry
          - category
        description: Switch to grouped rows (UI breakdown). Same filters. Pagination is over groups.
      - name: groupBy2
        in: query
        schema:
          type: string
          enum:
          - country
          - vertical
          - industry
          - category
        description: Optional nested dimension. Requires groupBy. Must differ from groupBy.
      - name: groupMetric
        in: query
        schema:
          type: string
          enum:
          - mean
          - median
          - max
          - min
          default: mean
        description: How metrics.* are aggregated. totals.* stay sums.
      - name: sort
        in: query
        schema:
          type: string
          enum:
          - visits
          - revenue
          - rating
          - domain
          - stores
      - name: order
        in: query
        schema:
          type: string
          enum:
          - asc
          - desc
      - $ref: '#/components/parameters/limit'
      - $ref: '#/components/parameters/offset'
  /v1/stores/compare:
    get:
      tags:
      - Stores
      summary: Side-by-side store cards (2–8 domains)
      operationId: compareStores
      parameters:
      - name: domains
        in: query
        required: true
        schema:
          type: string
        example: gruntstyle.com,titan.fitness,michaeltoddbeauty.com
  /v1/stores/{domain}:
    get:
      tags:
      - Stores
      summary: Unified store profile
      description: 'Includes `inventoryProbe` (StoreInventoryProbe or null): whether a Signal on this store could measure sales. Check `inventoryProbe.probable` before POST /v1/signals.'
      operationId: getStore
      parameters:
      - $ref: '#/components/parameters/domain'
  /v1/stores/{domain}/history:
    get:
      tags:
      - Stores
      summary: Daily store snapshots
      operationId: getStoreHistory
      parameters:
      - $ref: '#/components/parameters/domain'
      - name: limit
        in: query
        schema:
          type: integer
          default: 90
          maximum: 365
  /v1/stores/{domain}/traffic:
    get:
      tags:
      - Stores
      summary: Traffic series + latest SimilarWeb
      operationId: getStoreTraffic
      parameters:
      - $ref: '#/components/parameters/domain'
      - name: limit
        in: query
        schema:
          type: integer
          default: 24
          maximum: 120
  /v1/stores/{domain}/competitors:
    get:
      tags:
      - Stores
      summary: Ranked competitor stores (same list as the app's Competitors tab)
      description: 'Up to 24 stores ranked by `score`: product-type overlap (`sharedTokens`), traffic proximity (`trafficRatio`) and country/region/vertical match. `match.geo` is the pass that found it (country → region → global). Similarity ranking, not market share. Counts as a store detail view.'
      operationId: listStoreCompetitors
      parameters:
      - $ref: '#/components/parameters/domain'
      - name: limit
        in: query
        schema:
          type: integer
          default: 12
          minimum: 1
          maximum: 24
      responses:
        '200':
          description: Competitors
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CompetitorStore'
                  meta:
                    type: object
                    properties:
                      domain:
                        type: string
                      limit:
                        type: integer
                      count:
                        type: integer
  /v1/stores/{domain}/inventory-probe:
    get:
      tags:
      - Stores
      summary: Stock-readability verdict + per-product tags
      description: Scrape-time cart probe of the store's best sellers. `probable=true` only when `status=ok`; `capped` / `unlimited` / `blocked` mean a Signal cannot measure sales on this store. The same verdict is on GET /v1/stores/{domain} as `inventoryProbe`. 404 with hint when the store exists but was never probed.
      operationId: getStoreInventoryProbe
      parameters:
      - $ref: '#/components/parameters/domain'
      responses:
        '200':
          description: Verdict
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                    - $ref: '#/components/schemas/StoreInventoryProbe'
                    - type: object
                      properties:
                        products:
                          type: array
                          items:
                            $ref: '#/components/schemas/ProductInventoryProbe'
  /v1/stores/{domain}/contacts:
    get:
      tags:
      - Stores
      summary: Company info, emails, phones, LinkedIn
      description: Same reveal as the app contact drawer. Empty arrays mean no observation. Counts toward contactReveals plan bucket. emails lists verified addresses first and drops bounced ones; emailStatus maps verified addresses to valid | likely; people = named people with a verified mailbox {name, title, department, email, status, linkedinUrl}, leaders first, up to 15; verifiedCount = size of emailStatus.
      operationId: getStoreContacts
      parameters:
      - $ref: '#/components/parameters/domain'
      - name: department
        in: query
        schema:
          type: string
        description: 'Comma list to narrow people: leadership, sales, marketing, ecommerce, engineering, operations, finance, people, support, design, other'
  /v1/stores/{domain}/verify-contacts:
    get:
      tags:
      - Stores
      summary: Status of a contact verification run
      description: status none | pending | running | done | error; lastCheckedAt = last automatic or manual pass. Free.
      operationId: getVerifyContactsStatus
      parameters:
      - $ref: '#/components/parameters/domain'
    post:
      tags:
      - Stores
      summary: Find & verify contacts for one store
      description: Queues a site re-scan, a founder web search and an SMTP mailbox check (no email is sent). Usually 1-3 minutes; poll GET, then GET /contacts. 1 contactReveals; returns deduped:true (free) while a run is pending or within 1 hour of one finishing. 429 verify_queue_full when the shared queue is busy.
      operationId: requestVerifyContacts
      parameters:
      - $ref: '#/components/parameters/domain'
  /v1/contacts:
    get:
      tags:
      - Stores
      summary: Contacts by domain query (alias)
      operationId: getContactsByQuery
      parameters:
      - name: domain
        in: query
        required: true
        schema:
          type: string
        example: gruntstyle.com
components:
  schemas:
    CompetitorStore:
      type: object
      description: One ranked competitor. Similarity ranking (score), not market share.
      properties:
        domain:
          type: string
        url:
          type: string
        industry:
          type: string
        vertical:
          type: string
        category:
          type: string
        countryName:
          type: string
        monthlyRevenueUsdHigh:
          type: number
          description: Modeled high band, USD
        monthlyVisits:
          type: integer
        ecomScoutRating:
          type: number
        ecomScoutGrade:
          type: string
        productCount:
          type: integer
        visitsChangePct:
          type:
          - number
          - 'null'
        trafficRatio:
          type: number
          description: Competitor visits ÷ seed store visits
        tokenOverlap:
          type: integer
        sharedTokens:
          type: array
          items:
            type: string
        match:
          type: object
          properties:
            country:
              type: boolean
            region:
              type: boolean
            industry:
              type: boolean
            vertical:
              type: boolean
            category:
              type: boolean
            geo:
              type: string
              enum:
              - country
              - region
              - global
        score:
          type: number
    ProductInventoryProbe:
      type: object
      properties:
        handle:
          type: string
        title:
          type: string
        status:
          type: string
          enum:
          - ok
          - capped
          - unlimited
          - blocked
          - no_inventory
          - no_products
          - unknown
        attempted:
          type: integer
        measurable:
          type: integer
        capped:
          type: integer
        unlimited:
          type: integer
        failed:
          type: integer
        checkedAt:
          type: string
          format: date-time
    StoreInventoryProbe:
      type: object
      description: Scrape-time stock-readability verdict. `probable` is true only for status=ok. capped / unlimited / blocked = a Signal cannot measure sales here. null on the store profile = never probed.
      properties:
        status:
          type: string
          enum:
          - ok
          - capped
          - unlimited
          - blocked
          - no_inventory
          - no_products
        probable:
          type: boolean
        method:
          type: string
          enum:
          - json
          - cart_direct
          - cart_proxy
          - catalog_json
          - none
        attempted:
          type: integer
          description: Best-seller variants probed
        measurable:
          type: integer
          description: Variants with a real stock number
        capped:
          type: integer
          description: Variants that echoed the cart limit
        unlimited:
          type: integer
          description: Variants with no stock tracking
        failed:
          type: integer
        cartQtyCap:
          type:
          - integer
          - 'null'
          description: Detected per-order limit when capped
        checkedAt:
          type: string
          format: date-time
        fresh:
          type: boolean
          description: false when older than 14 days
        reason:
          type: string
          description: One sentence to quote to the user
      required:
      - status
      - probable
      - method
      - attempted
      - measurable
      - checkedAt
      - fresh
      - reason
  parameters:
    offset:
      name: offset
      in: query
      schema:
        type: integer
        default: 0
    domain:
      name: domain
      in: path
      required: true
      schema:
        type: string
      description: Bare domain or URL. gruntstyle.com and https://www.gruntstyle.com both work.
    limit:
      name: limit
      in: query
      schema:
        type: integer
        default: 20
        maximum: 100
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-Api-Key
    bearer:
      type: http
      scheme: bearer