# BuiltWith Alternative: Shopify Store List & Tech Stack Leads (`alexmorain/tech-stack-leads`) Actor

Lists of websites by technology: 1.5M Shopify stores, WooCommerce, Klaviyo and 7,286 more. Emails, phones and social links in each row. Filter by country, language, rank, keywords, AND / OR. No 10,000 cap. Watch for new sites, webhook. From $2 per 1,000.

- **URL**: https://apify.com/alexmorain/tech-stack-leads.md
- **Developed by:** [Alexis Morain](https://apify.com/alexmorain) (community)
- **Categories:** Lead generation, E-commerce
- **Features:** Limited permissions
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 websites

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## BuiltWith Alternative: Shopify Store List & Tech Stack Leads

Get lists of websites that use a given technology (Shopify, WooCommerce, Klaviyo, HubSpot, Magento and 7,286 more), with emails, phone numbers and social links in every row. Filter by country, language, domain rank and keywords, combine technologies with AND or OR, and watch for new sites on a schedule.

### What does it do?

You give one or more technology names. The actor returns every website where that stack was detected, one row per site, with the contacts found on the site and its full tech stack.

- **No 10,000 row wall.** Shopify in France alone is 19,952 stores. Shopify worldwide is 1,520,572. This actor pages past 10,000 rows with a cursor and goes up to 2,000,000 rows per run.
- **AND or OR between technologies.** "Shopify AND Klaviyo in France" returns 3,061 stores. "Shopify OR WooCommerce" runs one search per technology and removes duplicates.
- **Contact filter.** Ask for sites with an email, a phone or any social link, and the actor drops the rest before delivery. Dropped rows cost less than delivered ones (see pricing).
- **Freshness you can check.** Every row has `lastVisited`, the date the crawler last saw the site. `visitedAfter` keeps only sites seen after a date.
- **Watch mode.** Schedule the same search daily or weekly and get only the sites you never received before.
- **Count first.** `countOnly` returns the number of matching sites for about $0.02, before you spend anything on rows.

Results come back instantly: the data source answers live, there is no queue.

### Why use it?

Most store list actors stop at 10,000 rows per run. For Shopify in France that is half the market. For Shopify worldwide it is less than 1 %. Some have no technology filter at all, so "Shopify stores using Klaviyo" is not possible.

Most also charge you for rows without any contact. On the top 1,000 Shopify stores in France (run of November 9, 2026) we measured: email on 39 % of sites, phone on 29 %, email or phone on 46 %, at least one social link on 48 %. Smaller shops show less: on another sample of 100 stores, only 18 % had an email. Without a contact filter, 1,000 stores with an email means buying 2,500 to 5,500 rows at full price. With `contactFilter: "email"` you pay full price only for the 1,000 rows you keep.

These are our measured rates, not a marketing figure. They vary a lot by country, technology and site size.

Typical uses:

- Build a lead list of Shopify or WooCommerce stores in a country for an agency, an app or a SaaS.
- Find users of a competitor's tool (Klaviyo, Mailchimp, Intercom, Hotjar...) and pitch a switch.
- Size a market: how many sites in Germany run Magento, how many French sites run HubSpot.
- Get a weekly feed of newly detected stores for your outbound sequences.

### What data can I extract?

| Field | Example | Notes |
|---|---|---|
| `domain` | `example-shop.fr` | |
| `url` | `https://example-shop.fr` | |
| `title` | `Example Shop, lunettes de soleil` | page title |
| `description` | `Lunettes de soleil fabriquées en France...` | meta description |
| `domainRank` | `214` | 0 to 1000, higher is stronger. Null for about a quarter of sites |
| `country` | `FR` | ISO code, can be `WW` or null |
| `language` | `fr` | declared language |
| `contentLanguage` | `fr` | language detected in the content |
| `lastVisited` | `2026-11-02T00:00:00+00:00` | last crawl of the site |
| `email` | `contact@example-shop.fr` | first email, or null |
| `emails` | `["contact@example-shop.fr"]` | lowercased, deduplicated, valid syntax |
| `phone` | `+33 1 41 08 76 76` | first phone, or null |
| `phones` | `["+33 1 41 08 76 76"]` | HTML and street addresses removed |
| `socialUrls` | `["https://x.com/exampleshop"]` | broken links repaired, empty profiles removed |
| `facebook`, `instagram`, `linkedin`, `twitter`, `youtube`, `tiktok`, `pinterest` | `https://www.instagram.com/exampleshop` | first link per network, or null |
| `technologies` | `["Google Analytics", "Klaviyo", "Shopify"]` | flat sorted list |
| `techStack` | `{"ecommerce": {"ecommerce": ["Shopify"]}, ...}` | group, category, names |
| `technologyCount` | `23` | |
| `matchedTechnologies` | `["Shopify", "Klaviyo"]` | which of your technologies the site uses |
| `query` | `{"technologies": ["Shopify"], "countries": ["FR"], ...}` | the search that found the row |
| `isNew` | `true` | watch mode only, otherwise null |
| `scrapedAt` | `2026-11-09T14:02:11+00:00` | |
| `dataSource` | `DataForSEO` | |

With `countOnly`, each row is `{"type": "count", "technologies": [...], "filters": {...}, "totalCount": 19952}` and no site row is returned. Site rows carry `type: "website"`.

### How to get a list of websites by technology

1. Type the technology names in **Technologies**, as listed by the source (case does not matter). A typo stops the run before any paid call and suggests the closest names.
2. Pick **Several technologies**: `all` (sites using every technology) or `any` (sites using at least one).
3. Add countries, languages, keywords or a rank range if you need them.
4. Set **Only sites with contact details** if you only want sites with an email or a phone.
5. Set **Maximum sites** and start the run. Export to CSV, Excel or JSON, or read the dataset through the API.

Not sure of the exact name? The full list has 7,286 names grouped in 108 categories (for example `Shopify`, `WooCommerce`, `Klaviyo`, `HubSpot`, `Google Tag Manager`, `Stripe`). Close names are suggested when one is wrong.

### How much does it cost?

You pay per event:

| Event | Price |
|---|---|
| Actor start | $0.002 per run |
| Search page | $0.02 per call to the data source (up to 1,000 rows each, or one count) |
| Website (delivered row) | $3.00 per 1,000 on the free plan, $2.70 Bronze, $2.40 Silver, $2.00 Gold and above |
| Skipped website | $1.60 per 1,000: rows read but not delivered (contact filter, already sent by the watch, duplicate under OR) |

Examples on the free plan:

| Run | Approximate cost |
|---|---|
| Count all Shopify stores in France (`countOnly`) | $0.02 |
| 1,000 Shopify stores in France | $3.02 |
| 19,952 Shopify stores in France (all of them) | $60.26 |
| 1,000 Shopify stores in France with an email (about 2,600 rows read at 39 %) | about $6 |
| Weekly watch on Shopify France, 600 sites seen again, 100 new | $1.28 |

With a contact filter, the actor asks for small pages as it gets close to your target, so it never reads rows it will neither deliver nor charge. That adds a few search pages to a filtered run, and keeps a 10-row test run cheap.

Set **Max cost per run** in the Apify console to cap any run. The actor stops cleanly at that cap and never reads rows it cannot charge for.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `technologies` | list | | 1 to 10 technology names |
| `technologyMatch` | `all` or `any` | `all` | AND or OR between technologies |
| `countries` | list of ISO codes | empty | empty means worldwide |
| `languages` | list of codes | empty | language detected in the site content |
| `keywords` | list | empty | up to 10, all must appear in the title or description |
| `minDomainRank`, `maxDomainRank` | 0 to 1000 | empty | a minimum excludes sites without a rank |
| `visitedAfter` | date `YYYY-MM-DD` | empty | only sites crawled on or after this date |
| `sortBy` | `rank`, `recent`, `domain` | `rank` | strongest first, freshest first, or A to Z |
| `contactFilter` | `none`, `email`, `emailOrPhone`, `anyContact` | `none` | drop sites without contacts |
| `maxItems` | 1 to 2,000,000 | 1000 | rows delivered |
| `countOnly` | boolean | false | return counts only |
| `includeTechStack` | boolean | true | include `technologies` and `techStack` |
| `monitor` | boolean | false | watch mode, see below |
| `monitorName` | text | derived from the search | name of the watch |
| `webhookUrl` | URL | empty | receives a JSON summary at the end of the run |

With a contact filter, watch mode or `any`, the actor reads at most 20 times `maxItems` rows, then stops with a message. This keeps a rare filter from reading a whole country.

Sites whose country is unknown (`WW` or null) only show up when no country is selected.

Example input:

```json
{
  "technologies": ["Shopify", "Klaviyo"],
  "technologyMatch": "all",
  "countries": ["FR", "BE"],
  "contactFilter": "email",
  "maxItems": 500
}
```

### Output

```json
{
  "type": "website",
  "domain": "example-shop.fr",
  "url": "https://example-shop.fr",
  "title": "Example Shop, lunettes de soleil",
  "domainRank": 214,
  "country": "FR",
  "contentLanguage": "fr",
  "lastVisited": "2026-11-02T00:00:00+00:00",
  "email": "contact@example-shop.fr",
  "emails": ["contact@example-shop.fr"],
  "phone": "+33 1 41 08 76 76",
  "phones": ["+33 1 41 08 76 76"],
  "instagram": "https://www.instagram.com/exampleshop",
  "twitter": "https://x.com/exampleshop",
  "technologies": ["Google Analytics", "Klaviyo", "Shopify"],
  "technologyCount": 3,
  "matchedTechnologies": ["Shopify", "Klaviyo"],
  "isNew": null,
  "dataSource": "DataForSEO"
}
```

The dataset has four views in the console: **Websites**, **Contacts**, **Tech stack** and **Counts**.

### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("alexmorain/tech-stack-leads").call(run_input={
    "technologies": ["WooCommerce"],
    "countries": ["US"],
    "contactFilter": "emailOrPhone",
    "maxItems": 1000,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["domain"], item["email"], item["phone"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('alexmorain/tech-stack-leads').call({
    technologies: ['HubSpot'],
    countries: ['DE'],
    maxItems: 1000,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length);
```

cURL (waits for the run and returns the rows):

```bash
curl -X POST "https://api.apify.com/v2/acts/alexmorain~tech-stack-leads/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"technologies": ["Shopify"], "countries": ["FR"], "maxItems": 100}'
```

### Watch for new sites

Turn on `monitor` and schedule the run (daily or weekly). The first run delivers the usual list and remembers every domain it sent. Each later run asks only for sites crawled since the previous run, and delivers the ones you never received, with `isNew: true`. Sites you already have are counted as skipped websites.

Be clear on what "new" means. The data source gives no adoption date. The watch returns sites that were detected or crawled again since the last run and that you never received. On Shopify France, about 3 % of stores are crawled again in a week, so a weekly watch reads a few hundred rows and costs about a dollar.

Several watches can live side by side: give each one its own `monitorName`. Changing the search under the same name mixes the history, so use a new name for a new search.

Add `webhookUrl` to receive a summary at the end of each run (run id, dataset id, rows delivered, rows skipped). Plug it into Zapier, Make, n8n or your CRM.

### Tips

- Run `countOnly` first. A count costs $0.02 and tells you how big the full list is.
- Use `sortBy: "recent"` with `visitedAfter` for the freshest sites, or `sortBy: "rank"` for the biggest ones first.
- A minimum rank removes sites without a rank (about 26 % of them). Leave it empty if you want small shops.
- For outbound, `contactFilter: "email"` costs less than buying raw rows and filtering in a spreadsheet, and the gap grows as the email rate drops.
- `languages` uses the detected content language, so `["fr"]` with `["BE", "CH"]` targets French-speaking sites in Belgium and Switzerland.

### FAQ

**Where does the data come from?**
From the DataForSEO technology database, used under an agreement with DataForSEO. It is built by a crawler that reads public pages and detects technologies from their code.

**Why do my numbers differ from BuiltWith or another tool?**
Each database crawls a different set of sites and counts differently. Some lists announce 3.5 million Shopify stores, others 950,000. This one finds 1,520,572 today. Run `countOnly` to get the exact number for your search before you buy.

**Is it legal to collect these emails?**
The actor only returns contacts published on public websites, but public does not mean free of rules. In the EU and the UK, an email or phone number is personal data as soon as it identifies a person (`jane.doe@shop.com`, a mobile number), and generic addresses like `contact@` can still fall under it for a sole trader. Once you download the rows, you are the data controller under the GDPR, not this actor and not Apify.

In practice, for B2B prospecting:

- Rely on legitimate interest, and write down why the offer is relevant to the business you contact.
- Tell people where you got their details, at the latest in your first message.
- Give an easy way to opt out (an unsubscribe link or a reply), honor it right away, and keep a suppression list so a later run does not bring them back.
- Delete data you no longer use, and answer access or deletion requests.
- Follow the local rules on unsolicited email and calls (ePrivacy, CAN-SPAM in the US, do-not-call lists such as Bloctel in France).

This is not legal advice. Check with your counsel before a large campaign.

**Can I check the stack of a list of domains I already have?**
Not in this version. This actor goes from a technology to the sites, not from a site to its technologies.

**What happens if a run is interrupted?**
The actor saves its position and resumes where it stopped. Rows already delivered are skipped and not charged again.

**The run failed with "unknown technology". Did I pay?**
Only the actor start ($0.002). Names, countries and languages are checked before any paid call. The error message lists the closest valid names.

# Actor input Schema

## `technologies` (type: `array`):

1 to 10 technology names, for example <code>Shopify</code>, <code>WooCommerce</code>, <code>Klaviyo</code>, <code>HubSpot</code>, <code>Stripe</code>. Capitals do not matter, but the name must exist in the DataForSEO technology list (about 7,300 names): an unknown name stops the run before anything is read, with suggestions. How several technologies combine is set below.

## `technologyMatch` (type: `string`):

<b>All</b> (default) is one search: only sites that use every technology you listed (Shopify <i>and</i> Klaviyo). <b>Any</b> runs one search per technology and removes duplicates: a site already delivered earlier in the run is read again but not delivered, and billed as a skipped website.

## `countries` (type: `array`):

Keep sites detected in these countries. <b>Leave empty for the whole world.</b> Some sites are marked <code>WW</code> (worldwide) or have no country: they only come back when no country is chosen.

## `languages` (type: `array`):

Keep sites whose page content is written in these languages (the language DataForSEO detected in the content, not the HTML <code>lang</code> tag). Leave empty for any language.

## `keywords` (type: `array`):

Up to 10 words or phrases. <b>Every keyword must appear</b> in the home page title or description (AND): <code>coffee</code> and <code>tea</code> returns sites that mention both. Leave empty for no keyword filter.

## `minDomainRank` (type: `integer`):

DataForSEO domain rank on a <b>0-1000</b> scale (not the Ahrefs DR). About a quarter of sites have no rank at all: <b>setting a minimum removes them</b>.

## `maxDomainRank` (type: `integer`):

Upper bound of the 0-1000 domain rank. Sites with no rank are removed when a maximum is set.

## `visitedAfter` (type: `string`):

Keep sites the DataForSEO crawler visited on or after this date (<code>YYYY-MM-DD</code>). A recent visit means fresher contact details. About 3% of a large technology list is revisited each week.

## `sortBy` (type: `string`):

Order in which sites are read. With a limit on the number of sites, this decides which ones you get.

## `contactFilter` (type: `string`):

Filtered by the Actor after reading. Roughly 18% of sites have an email and 24% an email or a phone. <b>Billing:</b> every row read and dropped by this filter is billed as a <b>skipped website</b> (cheaper than a delivered website), and the Actor stops after reading 20 times your maximum number of sites.

## `maxItems` (type: `integer`):

Number of sites to deliver at most, for the whole run. Start small (100) to check the data, then raise it.

## `countOnly` (type: `boolean`):

Returns how many sites match, without any site row: one row per search with <code>totalCount</code>. Use it to size a market or check the cost before a big run. Billed one <b>search page</b> per search (one search, or one per technology when several technologies are combined with <i>any</i>).

## `includeTechStack` (type: `boolean`):

Adds <code>technologies</code> (flat list) and <code>techStack</code> (by group and category) to each row. Turn off for lighter rows; matched technologies and the technology count stay.

## `monitor` (type: `boolean`):

Schedule the Actor and pay only for new sites. The first run delivers up to your maximum, as usual. Later runs read only sites the crawler detected or revisited since the last run, and <b>never deliver the same domain twice</b>. Sites already received are read but not delivered, and billed as skipped websites. <b>DataForSEO gives no adoption date</b>: a site can be new to you without being new to the technology. The memory is kept in a named store, <code>tech-stack-leads-watch</code>, in your account.

## `monitorName` (type: `string`):

Name of the memory. Leave empty to derive it from the technologies and filters: same search, same memory. Use a name to keep one watch when you change the search.

## `webhookUrl` (type: `string`):

Optional. The run summary is sent there by POST when the run ends (Slack, Discord, Zapier...). The URL is never written to a log.

## Actor input object example

```json
{
  "technologies": [
    "Shopify"
  ],
  "technologyMatch": "all",
  "countries": [
    "FR"
  ],
  "sortBy": "rank",
  "contactFilter": "none",
  "maxItems": 1,
  "countOnly": false,
  "includeTechStack": true,
  "monitor": false,
  "monitorName": ""
}
```

# Actor output Schema

## `results` (type: `string`):

All rows in the default dataset.

## `summary` (type: `string`):

Searches, rows read, delivered and skipped, events charged, watch memory.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "technologies": [
        "Shopify"
    ],
    "countries": [
        "FR"
    ],
    "maxItems": 1
};

// Run the Actor and wait for it to finish
const run = await client.actor("alexmorain/tech-stack-leads").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "technologies": ["Shopify"],
    "countries": ["FR"],
    "maxItems": 1,
}

# Run the Actor and wait for it to finish
run = client.actor("alexmorain/tech-stack-leads").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "technologies": [
    "Shopify"
  ],
  "countries": [
    "FR"
  ],
  "maxItems": 1
}' |
apify call alexmorain/tech-stack-leads --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,alexmorain/tech-stack-leads"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/yGjVRo5OCd71ziT22/builds/V1rEhbNq7JRN2xqzU/openapi.json
