# AliExpress Search Scraper (`apt_marble/aliexpress-search-scraper`) Actor

Search AliExpress by keyword and get every result as a clean row: price, pre-discount price, star rating, delivery window, ships-from country, Choice status — and the exact units sold instead of the rounded "10,000+" label. Choose the country you shop from and prices arrive in its currency.

- **URL**: https://apify.com/apt\_marble/aliexpress-search-scraper.md
- **Developed by:** [Hamza](https://apify.com/apt_marble) (community)
- **Categories:** E-commerce
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.70 / 1,000 product scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## AliExpress Search Scraper

Search AliExpress the way a shopper does, and get the whole result list back as
a clean table instead of a web page. Every product comes with its price, its
pre-discount price, the discount, the star rating, the delivery window, where it
ships from, whether it is a Choice item — and **the exact number of units sold**,
not the rounded "10,000+ sold" label the store shows. One product on a recent run
displayed "10,000+ sold" while the store's own figure was **33,937**. That is the
difference between guessing at demand and measuring it.

Built for people who need real numbers out of a marketplace: product researchers
sizing demand, sellers watching a niche, agencies pricing against competitors,
and anyone building a catalogue.

### What you can do with it

- **Size demand for a niche.** Sort by units sold and see exactly how many each
  product has moved, so you can rank a category by real volume rather than by
  marketing badges.
- **Track a competitor's pricing.** Re-run the same terms on a schedule and
  watch price, pre-discount price and discount depth move over time.
- **Find winning products.** Filter to items above a star rating and above a
  sales floor, and get only the products that are already proven.
- **Compare markets.** Run the same term for several countries and see how
  each market's own selection, pricing and delivery estimates differ. Each
  country is a separate catalogue rather than the same list re-priced, so read
  them as two markets — every row is stamped with the country and currency it
  came from.
- **Separate real ranking from paid placement.** Promoted products are flagged
  and given no organic position, so you can see where a product truly ranks.
- **Build a product feed.** Pull thousands of rows per search term with images,
  titles, category path and links ready to load somewhere else.

### What you get

One row per product. An abridged real row:

```json
{
  "keyword": "wireless earbuds",
  "sortOrder": "best_match",
  "country": "US",
  "currency": "USD",
  "page": 1,
  "position": 1,
  "organicPosition": 1,
  "matchQuality": "exact",
  "productId": "3256806779925038",
  "globalProductId": "1005006966239790",
  "skuId": "12000038883449396",
  "title": "Digital Display Bluetooth Earphones with Mic TWS E6S Wireless Bluetooth Headset Noise Cancelling Headset for Xiaomi Huawei Oppo",
  "imageUrl": "https://ae-pic-a1.aliexpress-media.com/kf/Sc752fce18a1247459d9bf29ae6914e6dr.jpg",
  "price": 0.99,
  "originalPrice": 6.97,
  "discountPercent": 85,
  "priceFormatted": "US $0.99",
  "rating": 4.9,
  "soldCount": 119539,
  "soldCountDisplay": "100K+ sold",
  "shipsFromCountry": "CN",
  "deliveryMinDays": 6,
  "deliveryMaxDays": 11,
  "deliveryEarliestDate": "2026-08-15",
  "deliveryLatestDate": "2026-08-20",
  "categoryPath": ["44", "100000306", "63705"],
  "categoryId": "63705",
  "isChoice": true,
  "isPromoted": false,
  "hasFreeShippingOffer": true,
  "freeShippingNote": "Free shipping",
  "launchDate": "2024-05-13",
  "collectedAt": "2026-08-09T12:00:00.000Z"
}
```

### Input reference

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `keywords` | list of text | `["wireless earbuds"]` | What to search for. One term per line, up to 50 terms per run. |
| `country` | choice | `us` | Which country's version of the store to read. Sets the currency, the delivery estimates and **which products are offered** — each country has its own selection. |
| `sort` | choice | `best_match` | How the store orders the results: best match, most orders, price low to high, price high to low, or "widest coverage", which combines all four and merges them into one deduplicated list. |
| `maxResultsPerKeyword` | whole number | `200` | Stop after this many distinct products per search term. A single ordering is held to about 1,198 — the most the store publishes for one ordering; "widest coverage" raises the ceiling to about 3,424. |
| `maxPagesPerSort` | number | `10` | How deep to go for each ordering. Maximum 60. |
| `minPrice` | whole number | empty | Keep only products at or above this price, in the currency of the country you chose. |
| `maxPrice` | whole number | empty | Keep only products at or below this price, in the currency of the country you chose. |
| `minRating` | number | `0` | Keep only products rated at least this many stars. Unrated products are dropped when this is above 0. |
| `minSold` | whole number | `0` | Keep only products that have sold at least this many units, judged on the exact figure rather than the rounded label. Products with no published sales figure are dropped when this is above 0. |
| `freeShippingOnly` | yes/no | `false` | Ask the store for only products it offers with free shipping to your chosen country. |
| `shipFromCountry` | text | empty | Two-letter code of the warehouse the product ships from, for example `CN`, `US` or `ES`. |
| `categoryId` | text | empty | Narrow the search to one category, using the number from that category's own address. |
| `includePromoted` | yes/no | `true` | Keep promoted placements (always flagged) or drop them entirely. |
| `excludeLooseMatches` | yes/no | `false` | Drop rows from result pages the store filled with loosely related products instead of real matches. |
| `parallelSearches` | whole number | `2` | How many search terms to work through at once. Maximum 4. Each term collects the same products whatever this is set to; a wider setting only decides how many terms are in flight together, and the store slows itself down when pushed too hard. |

### Output fields

| Field | Type | Description |
| --- | --- | --- |
| `keyword` | text | The search term this row came from. |
| `sortOrder` | text | Which ordering produced this row. |
| `country` | text | The country the store was read as. |
| `currency` | text | Currency of `price` and `originalPrice`. |
| `page` | number | Which result page the row appeared on. |
| `position` | number | Rank on the result list, counting every card. |
| `organicPosition` | number | Rank ignoring promoted placements. Empty for a promoted row. |
| `matchQuality` | text | `exact` when the store reported a real keyword match, `loose` when it filled the page with related products instead. |
| `productId` | text | The product's identifier in the country you chose. |
| `globalProductId` | text | The product's identifier that stays the same across countries. Join on this one when merging two countries. |
| `skuId` | text | The identifier of the specific variant the listed price belongs to. |
| `title` | text | Product title. |
| `productUrl` | text | Direct link to the product on the store. |
| `imageUrl` | text | Main product image. |
| `price` | number | Current price in `currency`. Empty on the occasional listing that publishes no price; those rows are dropped when you set a price limit. |
| `originalPrice` | number | Price before the discount, where the store publishes one. |
| `discountPercent` | number | Discount depth, 1–99. |
| `priceFormatted` | text | Price exactly as the store displays it. |
| `rating` | number | Star rating out of 5. Empty when nobody has rated the product. |
| `soldCount` | number | **Exact units sold.** Empty when the store publishes no figure. |
| `soldCountDisplay` | text | The rounded label the store shows a shopper, kept for reference. |
| `shipsFromCountry` | text | Two-letter code of the warehouse the item ships from. |
| `deliveryMinDays` / `deliveryMaxDays` | number | Fastest and slowest estimated delivery, in days. Empty where the store publishes no estimate for that country. |
| `deliveryEarliestDate` / `deliveryLatestDate` | text | The same estimate as dates, and empty in the same cases. |
| `categoryPath` | list | Category identifiers from broadest to narrowest. |
| `categoryId` | text | The narrowest category identifier. |
| `isChoice` | yes/no | Whether the product is in the store's Choice programme. |
| `isPromoted` | yes/no | Whether the product is a paid placement inside the results. |
| `hasFreeShippingOffer` | yes/no | Whether the listing advertises free shipping to your chosen country. |
| `freeShippingNote` | text | The exact wording of that offer, including any minimum spend. |
| `badges` | list | Every promotional label on the card, with its wording. |
| `launchDate` | text | The date the product was first listed. This is not a "last updated" date. |
| `collectedAt` | text | When this row was collected. |

A run summary is also written to the run's own `OUTPUT` record: how many products
each term produced, how deep it went, how many rows were dropped by your filters,
the related searches harvested from the store's own suggestion cards, and — kept
strictly apart — which terms genuinely returned nothing and which ones could not
be read at all.

### Pricing

You pay per product row, and nothing else.

| What you pay for | Price |
| --- | --- |
| Each product row returned | $0.0007 (that is $0.70 per 1,000 products) |

Worked example: five search terms, 200 products each, is 1,000 rows and costs
**$0.70**. A deep single-term sweep using "widest coverage" to reach the full
3,424 products costs **$2.40**. Rows dropped by your own filters are never
charged, because they are never returned.

### Limits & what this actor cannot do

- **The store publishes a limited number of public results for any one search.**
  A single ordering reaches about **1,198 distinct products** (20 per result
  page, 60 pages). Combining all four orderings reaches about **3,424 distinct
  products** for one term — that is everything the store makes public for that
  term, and no setting can go past it. For a bigger harvest, search several
  narrower terms rather than one broad one.
- **There is no store or seller information on a search result, and no review
  count.** Search rows carry the product, its price and its performance — not
  the shop behind it. Those columns are not emitted at all rather than emitted
  permanently empty. There is also no numeric shipping cost: free shipping
  appears as the store's own wording, and paid shipping is not quoted here.
- **Prices and availability differ by country, and so does the product
  selection.** Two countries return genuinely different products for the same
  term, not the same products at converted prices. Results reflect the country
  you select, and every row says which one it was.
- **The displayed price is often a promotional or first-time-shopper price**,
  not a stable list price. Treat it as what a shopper would be offered today.
- **Prices, stock, ratings and rankings are a snapshot** at the moment of
  collection and keep changing afterwards. The store also reshuffles its own
  ordering between reads, so `position` is that moment's rank, not a stable one.
- **Promoted placements appear inside the store's own results.** They are
  flagged, given no organic position, and can be excluded.
- **A term with few real matches is filled out with loosely related products.**
  Those rows are marked `loose` rather than passed off as matches, and can be
  dropped — but the store does not always admit when it is doing this, so treat
  a surprisingly rich result for an obscure term with care.
- **An exact hyphenated phrase cannot be searched.** The store's own search
  treats `usb-c cable` as `usb c cable`, and apostrophes and slashes are
  mangled before the search ever runs, so they are removed from your terms.
- **A small number of listings publish no price.** They are returned with the
  price left empty rather than filled with a zero, and they are dropped
  automatically whenever you set a price limit.
- **Delivery estimates are not published everywhere.** Some countries' versions
  of the store quote a delivery window on every product and others quote none at
  all; where none is published, those columns are empty rather than guessed.
- **The store occasionally answers as a different market than the one asked
  for.** Every row always states the country and currency it is actually in, and
  the run says so plainly in its summary when this happens — so a price limit is
  never quietly applied in the wrong currency without you being told.
- **Total-result figures shown by the store are estimates** that drift between
  reads, so this actor does not return one. Count the rows instead.
- Speed depends on the size of the job and on the store's own response times.
  No fixed throughput is promised.
- The source site's terms govern automated access. You are responsible for using
  the data lawfully and in line with those terms.

### FAQ

**Do I need an account on AliExpress?**
No. Nothing is signed in and no account is involved.

**Does it need my login or password?**
No. The actor never asks for credentials of any kind and cannot see your
AliExpress account.

**Can I schedule it?**
Yes. Schedule it on Apify like any other actor. Re-running the same terms daily
or weekly is the intended way to track price and demand over time.

**Is the data complete?**
It is complete up to what the store publishes for a search term — about 1,198
products per ordering and about 3,424 across all four. That is a ceiling of the
site, not of the actor. Anything the actor could not read is reported separately
from terms that genuinely had nothing to return, so a quiet zero never gets
mistaken for a real answer.

**Why is `soldCount` sometimes empty when `soldCountDisplay` has a value, or the
other way round?**
Because the store does not always publish both. Where it publishes the exact
figure, you get it; where it publishes only the rounded label, you get that and
`soldCount` stays empty rather than being guessed at from the label.

**Which product identifier should I join on?**
Use `globalProductId` if you are merging results from more than one country —
`productId` is specific to the country's own catalogue and the same physical
product carries a different one elsewhere. Within a single country either works.

**Should I turn `parallelSearches` up?**
It will not change what you collect — each search term is worked through in
order and returns the same products either way. It only decides how many terms
run side by side, and the store slows itself down when pushed, so past the
default of 2 the gain shrinks quickly. 4 is the ceiling.

# Actor input Schema

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

What to search for on AliExpress. One term per line. Up to 50 terms per run. Apostrophes and slashes are removed before searching because the store's own search mangles them, and a term made only of punctuation is skipped.

## `country` (type: `string`):

Which country's version of the store to read. This decides the currency, the delivery estimates and which products are offered — each country has its own catalogue, so two countries return different products for the same search term.

## `sort` (type: `string`):

How the store should order the results. "Widest coverage" runs all four orderings and merges them, which returns roughly three times as many distinct products for one search term and takes about four times as long.

## `maxResultsPerKeyword` (type: `integer`):

Stop after this many distinct products for each search term. A single ordering publishes about 1,198 distinct products and the run is held to that; choose "Widest coverage" to reach about 3,424, which is everything the store publishes for one search term.

## `maxPagesPerSort` (type: `integer`):

How deep to go into the results for each ordering. The store publishes at most 60 result pages of 20 products for any one search.

## `minPrice` (type: `integer`):

Only keep products at or above this price, as a whole number in the currency of the country you selected. Leave empty for no minimum.

## `maxPrice` (type: `integer`):

Only keep products at or below this price, as a whole number in the currency of the country you selected. Leave empty for no maximum.

## `minRating` (type: `integer`):

Only keep products rated at least this many stars. Products nobody has rated yet are excluded when this is set above 0.

## `minSold` (type: `integer`):

Only keep products that have sold at least this many units. Uses the exact figure, not the rounded "10,000+ sold" label. Products for which the store publishes no sales figure are dropped when this is above 0.

## `freeShippingOnly` (type: `boolean`):

Ask the store to return only products it lists with free shipping to the country you selected.

## `shipFromCountry` (type: `string`):

Two-letter country code of the warehouse the product ships from, for example CN, US or ES. Leave empty for any origin.

## `categoryId` (type: `string`):

Optional. Narrow the search to one AliExpress category, using the number that appears in the category's own address. Leave empty to search everything.

## `includePromoted` (type: `boolean`):

Promoted placements sit inside the store's own results and are always flagged. Turn this off to keep only organic results.

## `excludeLooseMatches` (type: `boolean`):

When a search term has few or no real matches, the store fills the page with loosely related products. Turn this on to drop those rows instead of collecting them.

## `parallelSearches` (type: `integer`):

How many search terms to work through at the same time. Higher is faster, but the store slows itself down when pushed, so a very wide run spends time waiting rather than collecting. Each term still gets the same products either way. 2 is the balanced choice.

## Actor input object example

```json
{
  "keywords": [
    "wireless earbuds"
  ],
  "country": "us",
  "sort": "best_match",
  "maxResultsPerKeyword": 100,
  "maxPagesPerSort": 10,
  "minRating": 0,
  "minSold": 0,
  "freeShippingOnly": false,
  "shipFromCountry": "",
  "categoryId": "",
  "includePromoted": true,
  "excludeLooseMatches": false,
  "parallelSearches": 2
}
```

# Actor output Schema

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

Every product collected, one row per product.

## `runSummary` (type: `string`):

Per-search-term totals, related searches and anything that could not be read.

## `overview` (type: `string`):

The results in the run's own table view.

# 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 = {
    "keywords": [
        "wireless earbuds"
    ],
    "country": "us",
    "sort": "best_match",
    "maxResultsPerKeyword": 100,
    "maxPagesPerSort": 10,
    "minRating": 0,
    "minSold": 0,
    "freeShippingOnly": false,
    "includePromoted": true,
    "excludeLooseMatches": false,
    "parallelSearches": 2
};

// Run the Actor and wait for it to finish
const run = await client.actor("apt_marble/aliexpress-search-scraper").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 = {
    "keywords": ["wireless earbuds"],
    "country": "us",
    "sort": "best_match",
    "maxResultsPerKeyword": 100,
    "maxPagesPerSort": 10,
    "minRating": 0,
    "minSold": 0,
    "freeShippingOnly": False,
    "includePromoted": True,
    "excludeLooseMatches": False,
    "parallelSearches": 2,
}

# Run the Actor and wait for it to finish
run = client.actor("apt_marble/aliexpress-search-scraper").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 '{
  "keywords": [
    "wireless earbuds"
  ],
  "country": "us",
  "sort": "best_match",
  "maxResultsPerKeyword": 100,
  "maxPagesPerSort": 10,
  "minRating": 0,
  "minSold": 0,
  "freeShippingOnly": false,
  "includePromoted": true,
  "excludeLooseMatches": false,
  "parallelSearches": 2
}' |
apify call apt_marble/aliexpress-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,apt_marble/aliexpress-search-scraper"
        }
    }
}

```

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/JGUL7HJdVxj0D0irR/builds/pRCCVyZUdshWOemwp/openapi.json
