# ASOS Scraper - Products, Prices & Stock (`abotapi/asos-com-scraper`) Actor

Scrape ASOS by keyword, category or URL. Every row carries the selling price and the original price, the discount, brand, colour and selling-fast flags. Sizes, per-size stock and reviews are one toggle away. Recurring change tracking is first-class.

- **URL**: https://apify.com/abotapi/asos-com-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.70 / 1,000 product records

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

## ASOS Scraper: Products, Prices & Stock

Scrape ASOS, the global fast-fashion store: search by keyword, browse a
category, or paste any store URL to walk it directly. The catalogue is read
from the store's own JSON search API, one request per page of 48 products.

Every row carries the selling price **and** the original (pre-discount)
price, with the discount percentage computed, plus the brand, the colour and
the demand flags (new in, selling fast, restocking soon). Sizes, per-size
stock and customer reviews are one toggle away.

### What you get

| Field | Meaning |
|---|---|
| `recordId` / `productId` | The store's own product id, stable across runs |
| `title` / `brand` / `productCode` | Product name, brand, style code |
| `price` / `priceText` | The current selling price, as a number and as shown |
| `originalPrice` | The original (pre-discount) price |
| `onSale` / `discountPercent` | True when the selling price is below the original price, and by how much |
| `currency` / `colour` / `colourWayId` | Quoting currency and the colour way |
| `isNew` / `isSellingFast` / `isRestockingSoon` / `isPromotion` | The store's demand flags from the listing card |
| `imageUrl` / `url` | Product image and product page |
| `description` / `productType` | With Fetch product details on |
| `sizes` | With Fetch product details on: every size with brand size, SKU, EAN and per-size stock (`available`, `inStock`, `isLowInStock`, `isRestockingSoon`) |
| `inStock` / `seller` | With Fetch product details on: overall availability and the seller of record |
| `reviews` | With Fetch reviews on: rating count, average, Fit/Quality scores and the reviews themselves |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` | Incremental-mode change tracking fields |

### How to use it

**Search mode** (default): give it search terms and it walks the store's own
search results. Combine with the brand, colour and ordering filters; those
are sent to the store with every page, so a filtered run reads fewer pages
rather than throwing rows away afterwards. The price bounds and Discounted
rows only are applied to the rows the run reads, because the store offers no
parameter for them.

**URL mode**: paste any store URL. A product URL returns that single product
in full. A search or category URL is walked page by page, and the pasted URL
decides its own scope:

- `https://www.asos.com/search/?q=midi+dress` (add `&page=3` to start further in)
- `https://www.asos.com/women/dresses/cat/?cid=8799`

#### Notes on scope

- The brand, colour and ordering filters apply to every walk in both modes;
  a pasted product URL is the exception, it returns that one product.
- Brand and colour ids are the store's own numeric ids. Each row carries its
  `brandId`, so scrape any product once to look an id up. A malformed token
  is discarded rather than sent, so a typo can never silently widen a run
  back to the unfiltered catalogue.
- Rows are priced in GBP from the store's international (COM) storefront
  with the UK size scale; every row states its own currency.
- The store does not serve a stock state on its listing cards, so `inStock`
  and per-size availability arrive only with Fetch product details on.
- The declared result count moves by a few hundred between identical calls
  seconds apart, because the catalogue is live. The walk ends on the empty
  page, never on that count.

### Output

Dataset rows are flat JSON. Example (illustrative placeholder, not a real
product):

```json
{
  "recordId": "00000001",
  "rowType": "product",
  "productId": "00000001",
  "title": "Sample Name Tiered Midi Dress",
  "brand": "Sample Brand",
  "colour": "Navy",
  "price": 45.0,
  "priceText": "£45.00",
  "originalPrice": 60.0,
  "onSale": true,
  "discountPercent": 25.0,
  "currency": "GBP",
  "isNew": false,
  "isSellingFast": true,
  "imageUrl": "https://images.example.com/products/sample-dress/00000001-1.jpg",
  "url": "https://www.example.com/sample-brand/sample-name-tiered-midi-dress/prd/00000001",
  "scrapedAt": "2026-01-01T00:00:00Z"
}
```

With **Fetch product details** on, rows also carry `description`,
`productType`, `inStock`, `seller` and `sizes`, for example:

```json
{
  "sizes": [
    {"size": "XS - UK 6", "brandSize": "XS", "sku": "0000001", "ean": "0000000000000",
     "available": true, "inStock": true, "isLowInStock": false, "isRestockingSoon": false,
     "seller": "SAMPLE BRAND LTD"}
  ]
}
```

With **Fetch reviews** on, rows carry `reviews`: the store's declared rating
count, the average over the reviews page it serves, Fit and Quality scores,
and the reviews themselves. The store serves one page of reviews (about ten)
per product to automated readers, so Max reviews per product is a cap, not a
pagination promise.

With **Incremental mode** on, rows also carry `changeType` (NEW, UPDATED,
UNCHANGED, REAPPEARED, EXPIRED), `changedFields`, `firstSeenAt` and
`lastSeenAt`. Price moves and stock changes are reported as UPDATED.
**Resume from a previous run** continues one interrupted crawl without
returning or billing the records it already returned.

### Limits and costs

- `maxItems` is the single cap on a run (0 = unlimited). `maxPages` is an
  optional safety bound per source.
- **Fetch product details** and **Fetch reviews** each read extra data per
  record. They are a paid extra (the Product details & reviews event on the
  pricing tab), charged once per record at most even with both on. The price
  pair, brand, colour and demand flags are on every row either way.
- **Emit unchanged** and **Emit expired** return, and bill, extra rows; leave
  them off unless you want full snapshots or expiry tracking.

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

Results can be piped into the apps you already use through Model Context
Protocol (MCP) connectors, as an optional side channel. This never changes
the dataset output.

- `mcpConnectors`: authorize a connector under Apify, Settings, API &
  Integrations, then select it here. Notion receives a page per record;
  other connectors receive a best-effort write or digest. Each connector
  gets a condensed summary per record; the complete record always stays in
  the dataset.
- `notionParentPageUrl`: the Notion page under which record pages are
  created. Required for the Notion connector, ignored by the others.
- `maxNotifyListings`: cap on records written to each connector per run.

### Compute

This actor reads JSON over plain HTTP; 512 MB of memory is the default and
is plenty. A minimal run finishes in well under a minute.

# Actor input Schema

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

How to start the scrape. 'search' runs store searches. 'url' processes the store URLs you paste: a product URL is fetched as a single product, and a search or category URL is paged like a search, its own query or category deciding the scope. The filters below apply to every walk in both modes; a pasted product URL is the one exception and returns that single product in full.

## `searchTerms` (type: `array`):

One or more things to search ASOS for, for example 'midi dress', 'slim jeans', 'white trainers'. Each term is scraped independently. Search mode only: in URL mode the scope is read out of each pasted URL instead. Leave this empty and the run browses with only the filters applied.

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

Paste product URLs (https://www.asos.com/brand/product-name/prd/0000001, the short https://www.asos.com/prd/0000001 form works too), search URLs (https://www.asos.com/search/?q=midi+dress) or category URLs (https://www.asos.com/women/dresses/cat/?cid=8799). A pasted URL decides its own scope. Add \&page=3 (search URLs) or \&offset=48 (category URLs) to start the walk further in; a product URL ignores paging and returns that one product in full. The filters below narrow what the walks read: the brand, colour and ordering filters are sent to the store with every page, and the price bounds and Discounted rows only are applied to the rows this run reads.

## `brandIds` (type: `array`):

Only these brands, as the store's own numeric brand ids, for example 15370 (Bershka). Comma-tolerant: the store accepts a list and returns the union. The id is in each row's brandId field, so scrape any product once to look an id up. Applies in BOTH search mode and URL mode.

## `baseColourIds` (type: `array`):

Only these base colours, as the store's own numeric colour ids, for example 12 or a list like 10,12. The id is the store's internal base-colour token; each row carries its colour name, so verify an id with a one-row run before a large one. Applies in BOTH search mode and URL mode.

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

The store's own server-side ordering, sent with every page of every walk. This orders the walk itself (and therefore which rows a capped run keeps), not just the output. Applies in BOTH search mode and URL mode.

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

Only keep rows whose selling price is at least this many pounds. The store has no parameter for this, so it is applied to the rows this run reads. Applies in BOTH search mode and URL mode.

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

Only keep rows whose selling price is at most this many pounds. The store has no parameter for this, so it is applied to the rows this run reads. Applies in BOTH search mode and URL mode.

## `onSaleOnly` (type: `boolean`):

Only keep rows whose selling price is below the original price, that is rows with a discount. The store has no parameter for this, so it is applied to the rows this run reads. Applies in BOTH search mode and URL mode.

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

Fetch each item's product page for the description, the full size list (brand size and store size), and per-size stock availability including low-stock and restocking flags. Slower, and charged once per record. Leave off for a fast crawl: identity, prices, discount, brand and colour are included either way, without a stock state (the store does not serve one on its listing cards).

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

Fetch each item's customer reviews: the rating summary (average, count and the Fit/Quality score breakdown) and the most recent reviews up to Max reviews per product. Slower, and charged once per record under the same details surcharge (never twice, even with Fetch product details also on). Leave off by default.

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

Cap on review rows returned per product when Fetch reviews is on. The store serves one reviews page per product to automated readers, so this is a cap, not a pagination promise: rows beyond it are not fetched.

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

The single cap on this run: stop after collecting this many records across all search terms and URLs. Set 0 for unlimited.

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

Safety bound on how many result pages to walk per search term or URL. Leave empty (0) for no page limit: the run then stops at Max items, not here. Does not impose a separate cap below Max items.

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

Paste a previous run ID or dataset ID to continue a large crawl without returning or charging for records already collected there. Use this after an interrupted or capped run. For recurring monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily or weekly recurring monitoring of the same search, for example a price watch. The first run returns everything matching as NEW. Later runs normally return only NEW, UPDATED and REAPPEARED records. Price moves and stock changes ARE reported as UPDATED: on a store they are the signal, not noise. Turn on Emit unchanged or Emit expired only when you also want those rows returned (and billed). State is kept separately per search, URL and filter setup; use State key 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 search terms, URLs and filters, so two different searches never mix state.

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

Off by default. Turn on to also return records 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 a full snapshot every run.

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

Off by default. Turn on to also return records 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 items capped it, not when Resume was used, and not when the run was refused. This returns, and bills, extra synthetic rows, so leave it off unless you need expiry tracking.

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

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a page per record; other connectors get a best-effort write or digest. Each connector receives a condensed summary per record, not the full record; the complete record always stays in the dataset. Leave empty to skip; this never changes the dataset output. 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 record pages are created. Required to enable the Notion export; ignored by other connectors.

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

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

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

Leave the default for the cheapest run. The store serves ordinary shared connections, so the default pool is enough. Change it only if a run reports that it could not read any results.

## Actor input object example

```json
{
  "mode": "search",
  "searchTerms": [
    "midi dress"
  ],
  "urls": [
    "https://www.asos.com/women/dresses/cat/?cid=8799"
  ],
  "sortBy": "relevance",
  "onSaleOnly": false,
  "fetchDetails": false,
  "fetchReviews": false,
  "maxReviewsPerProduct": 10,
  "maxItems": 20,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50,
  "proxy": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `overview` (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",
    "searchTerms": [
        "midi dress"
    ],
    "urls": [
        "https://www.asos.com/women/dresses/cat/?cid=8799"
    ],
    "fetchDetails": false,
    "fetchReviews": false,
    "maxItems": 20,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/asos-com-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",
    "searchTerms": ["midi dress"],
    "urls": ["https://www.asos.com/women/dresses/cat/?cid=8799"],
    "fetchDetails": False,
    "fetchReviews": False,
    "maxItems": 20,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/asos-com-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",
  "searchTerms": [
    "midi dress"
  ],
  "urls": [
    "https://www.asos.com/women/dresses/cat/?cid=8799"
  ],
  "fetchDetails": false,
  "fetchReviews": false,
  "maxItems": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/asos-com-scraper --silent --output-dataset

```

## MCP server setup

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