# AliExpress Scraper - products, variants, prices & reviews (`abotapi/aliexpress-scraper`) Actor

Scrape AliExpress products: search by keyword or category, or process pasted search, category and product URLs page by page. Identity, price, availability, media and SKU fields come from the results page; optional detail and reviews steps add variant tables, specifications, seller data and reviews.

- **URL**: https://apify.com/abotapi/aliexpress-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.80 / 1,000 product results

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?

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 Scraper

Scrape product data from AliExpress (aliexpress.com): search by keyword or
category, or walk pasted search / category / product URLs forward page by page.
Every result includes identity, price, discount, currency, rating, units sold,
media, and seller fields; an optional detail step adds SKU variant tables,
specifications, description, seller data and more; an optional reviews step
adds per-product review records.

Covers the global storefront in 11 markets (US, UK, DE, FR, ES, IT, BR, CA, AU,
NL, PL) with prices rendered in that market's currency.

### Quick start

1. **Mode: search**  -  enter one or more keywords, optionally a category ID and
   price / free-shipping / sort filters, and set **Max products total**.
2. **Mode: URL**  -  paste search, category, or product URLs
   (`/w/wholesale-...html`, `/category/...html`, `/item/<id>.html`).
3. Choose whether to also **fetch detail pages** and/or **reviews**.
4. Run. Results land in the dataset; optionally pipe them into Notion, Linear,
   Airtable or any MCP app via the connectors section.

### What each run returns

Every product record includes:

| Group | Fields |
|---|---|
| Identity | `productId`, `productUrl`, `productName` (`title`), `productType`, `categoryId` |
| Price | `currentPrice` (`price`), `originalPrice`, `discountPct` (`discountPercentage`), `currency` |
| Rating & sales | `ratingScore` (`rating`), `reviewCount`, `itemSold` (`totalSold`), `itemSoldCount` (`totalSoldCount`) |
| Availability | `inStock`, `isSponsored` |
| Media | `primaryImage` (`imageUrl`), `imageGallery` (`additionalImages`) |
| Seller | `sellerName`, `sellerCompanyName`, `brandName` |
| Promotions | `sellingPoints` (badges such as "New shoppers save ...", "Best price ...") |
| Bookkeeping | `fetchedAt` (`scrapedAt`), `sourceUrl`, `seedType`, `seedValue`, `shippingCountry` |

When **fetchDetails** is on, each product additionally carries `skuList`
(per-variant SKU table with prices), `skuPropertyList` (size / color / model
options), `specifications`, `description`, `descriptionHtml`, `breadcrumbs`,
`shipping`, `warranty`, `sellerId`, `sellerPositiveRate`, `brandId` and the
detail `images`.

When **fetchReviews** is on (or **reviewsOnly**), the actor also emits one
record per review: `reviewId`, `buyerName`, `rating`, `reviewTime`,
`reviewContent`, `skuInfo` (the bought variant), `likeCount` and any `media`.
Products without reviews emit an empty reviews list and keep `rating` /
`reviewCount` as-is (only populated when the storefront reports them).

> Sample shape  -  values are illustrative placeholders, not from a live listing.
>
> ```json
> {
>   "type": "product",
>   "id": "1234567890123",
>   "productUrl": "https://www.aliexpress.com/item/1234567890123.html",
>   "productName": "Bluetooth Earbuds, Wireless Headphones with Noise Cancelling",
>   "currentPrice": 12.99,
>   "originalPrice": 45.9,
>   "discountPct": 72,
>   "currency": "EUR",
>   "ratingScore": 4.8,
>   "reviewCount": 2300,
>   "itemSold": "1,000+ sold",
>   "primaryImage": "https://ae-pic-a1.aliexpress-media.com/kf/S00000000000000000a.jpg",
>   "sellerName": "Example Store",
>   "sellingPoints": ["New shoppers save EUR 5.00", "Delivery: Aug 09 - 17"],
>   "fetchedAt": "2026-08-05T08:20:40.310Z"
> }
> ```

### Filters

- **Min / Max price**  -  in the market's currency; below/above the range is
  dropped after fetching.
- **Free shipping only**  -  requests only free-shipping eligible items.
- **Sort order**  -  best match (default), best selling, newest, price low→high,
  price high→low.
- **Minimum rating**  -  1-5; lower-rated products are dropped.
- **Category ID**  -  restrict a keyword search to one category.

Each filter is applied per run and also becomes part of the incremental
monitoring state key (see below), so differently-filtered runs never share
state.

### Resume and recurring updates

Two separate features:

- **Resume from a previous run**  -  paste a run ID or dataset ID to continue an
  interrupted walk without re-returning (or re-billing) products already
  collected there. Use it to extend one large pull across separate runs.
- **Incremental mode**  -  for daily / weekly monitoring of the same search. The
  first run marks everything NEW; later runs return only what changed:
  `NEW`, `UPDATED`, `REAPPEARED`, and (when enabled) `UNCHANGED` / `EXPIRED`,
  each with `changeType`, `changedFields`, `firstSeenAt` and `lastSeenAt`.
  State is stored per configuration. **Emit unchanged / Emit expired** return
  and bill extra rows  -  leave them off unless you need a full snapshot or
  expiry tracking. EXPIRED is only emitted after a run that fully scans the
  tracked search (not a capped, resumed, or reviews-only run).

### Limits

- **Max products total** is the one hard cap (default 3). Set to 0 for
  unlimited.
- **Max result pages** is optional; leave empty for no page limit (the run
  still stops at Max products total).
- **Fetch full product detail** and **Include reviews on each product** are
  both OFF by default and add a separate page fetch per product (slower,
  plus a per-product surcharge). We recommend leaving them off unless you
  specifically need SKU/spec/seller detail or review text - the default
  results-page fields already cover the common case.

### Connection notes

For the best results keep the default connection group selected in the
Connection section of the input. With other connection groups, runs typically
return 0 results; a warning is shown at startup. The connection country is
pinned to the Target market you select so prices stay in one currency.

Separately, AliExpress intermittently refuses a meaningful share of shared
exit connections with a small HTTP-200 "refusal shell" page instead of real
results (measured windows range from ~1 in 8 connections working to all of
them working - the decision is per-connection reputation at the site's edge).
The actor handles this by rotating, and the rotation is bounded and disclosed:

- **Launch budget: 12 browser launches per run** (the first connection plus
  fresh-exit retries). A clean launch is reused for every page and product of
  the run - rotation happens only on refusals, never per page or per case.
- **Per-fetch cap: any single result page is attempted at most 5 times** on
  the standard pool before it counts as refused.
- **Fallback connection pool: after half the launch budget is spent without a
  single clean response**, the remaining rotations continue on Apify's
  unblocking proxy pool (group: UNBLOCKER); the run log says so when it
  happens. That pool serves different exits and may price in another market's
  currency; it is a data-availability fallback, not a market switch.
- **A refused shell is never parsed as data**: refusals are counted, never
  billed, and never read as "end of results".
- **Worst case, a fully refused run keeps failing loudly**: when every
  connection is refused, the run FAILS with a clear error message rather than
  returning an empty dataset, so you can tell a real refusal apart from a
  genuinely empty search. If a run fails this way, simply retry it - a fresh
  connection is very likely to succeed. A partially refused run discloses the
  shortfall as a "Partial results" status message.

The test plan in `.actor/test_plan.json` carries a per-case `retryAllowance`
note: each full-test case may spend the run's whole launch budget on fresh
exits before it reports a genuine failure, so a case failure means the site
refused every connection that run, not that the code path is broken.

### Send results into your apps (MCP connectors)

The full record always stays in the Apify dataset. Optionally, the actor can
also pipe a condensed, human-readable summary of each item (title + key fields
flattened to plain text  -  not the full JSON) into apps you already use via
Model Context Protocol connectors. Authorize a connector once under
Apify → Settings → Integrations, then select it in the input. Three input
fields control this:

- **`mcpConnectors`**  -  the list of authorized connectors to pipe results
  into (Notion, Linear, Airtable, or Apify's own MCP server). Leave empty to
  skip.
- **`notionParentPageUrl`**  -  required only when Notion is selected: the
  Notion page under which item pages are created.
- **`maxNotifyListings`**  -  a per-connector cap on how many items are
  exported each run (default 50); it does not affect the dataset.

### Notes

- Prices are as rendered by the storefront for the chosen market and may
  include listing-time promotions.
- The storefront occasionally shows a "sold" figure as text (e.g.
  "1,000+ sold"); the numeric `totalSoldCount` is parsed from that text.

# Actor input Schema

## `mode` (type: `string`):

Pick how to start. 'search' runs the queries + filters below. 'url' walks one or more pasted search / category / product URLs forward, page by page. Product URLs emit a single product; search and category URLs walk their full result list.

## `shippingCountry` (type: `string`):

Which market to scrape. Pins the connection exit country and the currency/locale the storefront renders (prices come back in that market's currency). Defaults to the United States.

## `queries` (type: `array`):

One or more search terms. The actor runs a separate paginated walk for each keyword. Examples: 'airpods', 'led strip lights', 'phone case'. Only used when mode = search.

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

Optional: restrict the keyword search to one AliExpress category id (the numeric id in a category URL such as /category/200003409/... or in the search refine panel). Leave empty for all categories.

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

Request only items eligible for free shipping. Applied to the search request; on some results the storefront still mixes in non-eligible items.

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

Sort the search results. Best match is the storefront default.

## `urls` (type: `array`):

One or more AliExpress URLs. Search result pages (/w/wholesale-...html), category pages (/category/...html) and product pages (/item/<id>.html) are all accepted. Multi-URL supported. Each search/category URL is walked forward from its starting page; filters are baked into the pasted URL, not reapplied here (the price/rating filters below still apply). Examples: https://www.aliexpress.com/w/wholesale-earbuds.html, https://www.aliexpress.com/category/200003409/electronics.html, https://www.aliexpress.com/item/1234567890123.html.

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

Minimum price in the market's currency. Products below this are dropped. Applies to a keyword search and a pasted search or category URL. No bound when empty.

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

Maximum price in the market's currency. Products above this are dropped. Applies to a keyword search and a pasted search or category URL. No bound when empty.

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

Drop products whose average rating is below this value (1 to 5). Applied after the results are fetched. Applies to a keyword search and a pasted search or category URL, not just one mode.

## `reviewsOnly` (type: `boolean`):

When ON, the actor scans the results to discover product IDs but emits ONE record per review (type='review'), not one per product. Works in both modes.

## `fetchReviews` (type: `boolean`):

When ON and reviewsOnly is OFF, every product record additionally carries an embedded array of its top reviews (capped by Max reviews per product). This is a separate fetch per product - it adds real time and the same per-product detail-enrichment surcharge as Fetch full product detail below. We recommend leaving it OFF unless you specifically need review text; products with no reviews emit an empty list either way.

## `maxReviewsPerProduct` (type: `integer`):

Hard cap on reviews fetched per product.

## `fetchDetails` (type: `boolean`):

When ON, the actor visits each product's own detail page after the results, adding full SKU variant tables, specifications, description, seller data, and more. This is a separate fetch per product, so it meaningfully slows the run and adds a per-product detail-enrichment surcharge. We recommend leaving this OFF: the default (OFF) results-page fields already cover id, title, price, discount, rating, sold count, images and seller name, which is enough for most uses at much lower cost and speed. Turn ON only when you specifically need SKU variants, specs, or full seller data.

## `maxPages` (type: `integer`):

Optional bound on result pages fetched per keyword or pasted search/category URL (the storefront serves ~60 cards per page). Leave empty (0) for no page limit - the run still stops at Max products total.

## `maxListings` (type: `integer`):

Hard cap across all keywords / URLs. Set to 0 for unlimited (still bounded by Max result pages). When reviewsOnly = true, this caps DISCOVERED products, not reviews; total review records can be much higher.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a full walk without returning or charging for products already collected there (matched by product id). Use this after an interrupted run, or to keep extending one pull across separate runs. For recurring daily/weekly monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching products as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED products. Turn on "Emit unchanged" or "Emit expired" only when you also want those products returned (and billed). State is kept separately per mode + search/URL/filter configuration; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the mode + search/URL/filter settings - different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return products that have not changed since the last run, marked UNCHANGED. This returns - and bills - extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return products that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search - not when Max products total capped it, when Resume was used, or in reviews-only mode. This returns - and bills - extra synthetic rows, so leave it off unless you need expiry tracking.

## `proxy` (type: `object`):

Apify Residential is required for the best results (the storefront refuses many shared exits; the actor warns at startup if the default group is not selected). The connection country is pinned to the Target market you selected. If the site keeps refusing connections mid-run, the actor rotates fresh exits - up to 12 browser launches per run, with any single result page attempted at most 5 times - and after half the launch budget is spent without a clean response it continues on Apify's unblocking proxy pool (group: UNBLOCKER), which is disclosed in the run log when used. A run refused on every connection fails loudly rather than returning an empty dataset.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON - the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "shippingCountry": "us",
  "queries": [
    "airpods"
  ],
  "freeShippingOnly": false,
  "sortBy": "relevance",
  "urls": [
    "https://www.aliexpress.com/w/wholesale-earbuds.html"
  ],
  "reviewsOnly": false,
  "fetchReviews": false,
  "maxReviewsPerProduct": 100,
  "fetchDetails": false,
  "maxListings": 3,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

## `reviews` (type: `string`):

No description

# 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 = {
    "mode": "search",
    "shippingCountry": "us",
    "queries": [
        "airpods"
    ],
    "sortBy": "relevance",
    "urls": [
        "https://www.aliexpress.com/w/wholesale-earbuds.html"
    ],
    "maxPages": 0,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/aliexpress-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 = {
    "mode": "search",
    "shippingCountry": "us",
    "queries": ["airpods"],
    "sortBy": "relevance",
    "urls": ["https://www.aliexpress.com/w/wholesale-earbuds.html"],
    "maxPages": 0,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/aliexpress-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 '{
  "mode": "search",
  "shippingCountry": "us",
  "queries": [
    "airpods"
  ],
  "sortBy": "relevance",
  "urls": [
    "https://www.aliexpress.com/w/wholesale-earbuds.html"
  ],
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call abotapi/aliexpress-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/aliexpress-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/W5ruHPzqEYaUZgvFB/builds/RDvSEP096gg5rfkwB/openapi.json
