# ASOS Products Scraper (`scrapyx/asos-products-scraper`) Actor

Product listings from ASOS search and categories across 8 storefronts, via the site's own public JSON API: name, brand, price, was-price, EU 30-day lowest price, colour, images and stock signals.

- **URL**: https://apify.com/scrapyx/asos-products-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 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

## ASOS Products Scraper

Product listings from ASOS search and categories, across **8 storefronts** —
via the site's own public JSON API. No HTML parsing, so nothing here breaks
on a redesign.

Returns name, brand, price, was-price, **the EU 30-day lowest price**,
colour, product type, images and stock signals.

### Input

```json
{
  "mode": "search",
  "store": "uk",
  "keywords": ["running shoes", "denim jacket"],
  "sortBy": "priceAsc",
  "pageLimit": 200,
  "maxPages": 3
}
```

Or list a category by ID (a category URL containing `?cid=` works too):

```json
{ "mode": "category", "store": "de", "categoryIds": ["4209"] }
```

### Stores

`uk` `us` `de` `fr` `it` `es` `nl` `au` — each with its own currency,
language and catalogue. The same query returns **12,150** items on UK and
**8,035** on US, so the store is a real choice, not a display setting.

The API needs a matching store/currency/language/country set and rejects a
partial one, so only combinations that were called and confirmed are offered.

### What you get

| `recordType` | One per | Carries |
| --- | --- | --- |
| `SEARCH_SUMMARY` | keyword / category | upstream's own `itemCount`, pages fetched, rows returned, duplicates dropped, requested vs used page limit, relevance score |
| `PRODUCT` | product | id, code, URL, name, brand, type, colour, prices, images, stock flags — plus the raw upstream object |
| `ERROR` | failed input | why |

```jsonc
{
  "recordType": "PRODUCT",
  "productId": 206644707,
  "productUrl": "https://www.asos.com/on/on-cloudrunner-3-...",
  "name": "ON Cloudrunner 3 running trainers in green",
  "brandName": "On",
  "currency": "GBP",
  "price": 89.99, "previousPrice": 140.0,
  "lowestPriceInLast30Days": 98.0,
  "isMarkedDown": true,
  "colour": "GREEN", "productType": "Product"
}
```

`lowestPriceInLast30Days` is ASOS's own EU price-transparency figure — the
number a "was £140" claim is actually measured against. It is published on
the listing endpoint, so you get it without a per-product fetch.

### Known limits — read these before you rely on the output

**`pageLimit` above 200 is refused up front.** ASOS's ceiling is 200 and it
**clamps silently** — asking for 500 returns 200 rows with `HTTP 200` and no
warning of any kind. Bisected: 199 → 199, 200 → 200, **201 → 200**. This is
the one place the API is not explicit, so the actor refuses the value rather
than letting you believe you asked for more. Every summary reports
`pageLimitRequested` beside `pageLimitUsed`.

**ASOS fuzzy-matches an unmatched query rather than returning nothing** — but
it is honest about the size. `itemCount` drops from 12,150 for "shoes" to
**34** and **13** for two different nonsense strings, and the rows that come
back have internal code-like names. Both signals are surfaced:
`upstreamItemCount` is passed through, and `titleMatchRate` measures how many
returned products actually mention a query token (name, brand, product type).
`lowRelevanceWarning` flags a run below 20%. Rows are never dropped for it.

**Everything else about this API is honest, which is worth stating.** A bad
sort, a bad store and a missing query each earn an `HTTP 400` whose body
*names the offending parameter*. Past the last page you get `200` with an
empty `products` array while `itemCount` still reports the true total. Offset
paging is genuine: offsets 0 and 48 share zero ids, and offset 0 fetched twice
is identical. Most of this actor's validation exists to fail sooner with a
better message, not to compensate for lies.

**`previousPrice` is absent on full-price items.** It was present on 49 of 60
in testing — items with no markdown genuinely have none. It is left `null`
rather than echoing `price`.

### Technical

`GET https://www.asos.com/api/product/search/v2/` — the same endpoint the web
client uses, anonymously: no key, no token, no signature. The mobile host
`api.asos.com` rejects the identical call with `400 "The Language field is
required"`; `www.asos.com` is the working host.

No WAF: 5/5 TLS profiles returned 200.

`robots.txt` was checked on **the API path itself**, not just the apex:
`/api/product/search/v2/` is ALLOWED, and the `User-agent: *` group disallows
only a handful of unrelated paths.

Full recon trail is in `CRAWLING_METHOD.md`.

# Actor input Schema

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

'search' queries ASOS's product search by keyword. 'category' lists a category by its numeric ID (or a category URL containing ?cid=).

## `store` (type: `string`):

Which ASOS storefront to query. Each one has its own currency, language and catalogue size — the same query returns 12,150 items on UK and 8,035 on US. The API needs a matching store/currency/language/country set, so only verified stores are offered.

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

Search terms, one per row. ASOS fuzzy-matches rather than returning nothing for an unmatched term, so each keyword's relevance is measured and its own itemCount reported.

## `categoryIds` (type: `array`):

ASOS numeric category IDs (e.g. 4209), or a category URL containing '?cid='. Used by the 'category' mode.

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

Result ordering. Verified to genuinely reorder results, and ASOS rejects an unknown value with HTTP 400 rather than silently ignoring it.

## `pageLimit` (type: `integer`):

How many products to ask for per API call. ASOS's real ceiling is 200 and it CLAMPS SILENTLY — asking for 500 returns 200 with HTTP 200 and no warning — so anything above 200 is refused here instead, and every summary reports 'pageLimitRequested' beside 'pageLimitUsed'.

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

How many pages to fetch per keyword or category, via offset paging. Past the end ASOS returns an empty products array honestly, which stops paging early.

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

Optional cap on total product rows across the run, shared across every keyword. Counted after de-duplication.

## `maxConcurrency` (type: `integer`):

How many requests may be in flight at once. Only matters when several keywords or categories are listed; paging within one is sequential.

## `minRequestInterval` (type: `number`):

The honest speed control: the shortest gap between two request starts. No WAF or rate limiting was observed (5/5 TLS profiles clean); this is routine pacing.

## `proxyConfiguration` (type: `object`):

Residential with no country pin is the cloud default. The store input decides the catalogue, language and currency, so the proxy country does not change your data.

## Actor input object example

```json
{
  "mode": "search",
  "store": "uk",
  "keywords": [
    "running shoes"
  ],
  "categoryIds": [],
  "sortBy": "relevance",
  "pageLimit": 200,
  "maxPages": 3,
  "maxConcurrency": 3,
  "minRequestInterval": 0.5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/asos-products-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/asos-products-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 '{}' |
apify call scrapyx/asos-products-scraper --silent --output-dataset

```

## MCP server setup

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