# John Lewis Scraper (`s-r/john-lewis-scraper`) Actor

Search johnlewis.com or paste a listing URL and get product rows: price, previous price and sale flag, rating, review count, stock status, brand, product and SKU id, colour variants and images. Read from the site's own JSON, so the reduction history comes with it.

- **URL**: https://apify.com/s-r/john-lewis-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** E-commerce, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.002 / actor run started

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?

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

## John Lewis Scraper

A John Lewis scraper that turns a search term or a listing URL into structured product rows: price, previous price, rating, review count, stock status, brand and every colour variant. No login, no cookie, no API key.

It reads the JSON that johnlewis.com already ships inside its own pages, which is why the rows carry fields the rendered cards never show, such as the reduction history behind a sale price and the full colour variant list.

### What you get

- **Price and previous price**, so a reduction is a number you can act on rather than a red badge you have to interpret. `on_sale` tells you whether the site records a reduction at all
- **Rating and review count** per product, straight from the site's own data
- **Stock status** as a boolean, not a guess from whether a button is greyed out
- **Every colour variant** with names and a count, plus the alternative image URLs
- **Brand, product id and SKU id**, so consecutive runs deduplicate cleanly and you can join against your own catalogue
- **Real pagination.** The site reports how many results and pages exist, so the walk stops at the true last page instead of hammering until it 404s
- **A named error when a term redirects.** Search for a brand like "dyson" and John Lewis sends you to a brand landing page with no products on it. Instead of an empty run, you get the destination URL and what to do about it
- **No actor-start fee.** A run that returns nothing costs nothing

### Why scrape John Lewis

John Lewis is a price anchor for the UK market. It rarely discounts first, it price-matches selectively, and its Never Knowingly Undersold heritage means competitors watch it closely even now that the promise has changed. If you sell homeware, electricals or fashion into the UK, what John Lewis lists and at what price is a reference point your own pricing has to answer to.

There is no public product API. The site is a Next.js application, so every listing page carries its data as JSON for the browser to render, and that JSON is richer than the page it produces. Reading it is both faster than driving a browser and more stable than chasing CSS class names that change with every deploy.

The third reason is the reduction history. A UK retailer's sale price only means something next to what the item cost before it, and that prior price is in the payload rather than on the page. Most scrapers throw it away because they read the card instead of the data behind it.

### Input

| Field | Type | Required | Default | What it does |
|---|---|---|---|---|
| `search_term` | string | yes | `laptop` | What to search for on johnlewis.com |
| `url` | string | no | – | A johnlewis.com search or category URL to read instead. Takes precedence over the search term |
| `limit` | integer | no | `48` | Products to return, 1 to 480 |
| `sort_by` | select | no | `relevance` | `relevance`, `price_low_high`, `price_high_low`, `newest`, `popularity`, `discount` |
| `retries` | integer | no | `3` | Retry attempts per page |

### Output

```json
{
  "position": 1,
  "product_id": "113493667",
  "sku_id": "113397136",
  "title": "Acer Chromebook 314 Laptop, Intel Celeron Processor, 4GB RAM, 64GB eMMC, 14\" Full HD, Black",
  "brand": "Acer",
  "url": "https://www.johnlewis.com/acer-chromebook-314-laptop.../p113493667",
  "price": 169.99,
  "price_max": 169.99,
  "price_display": "£169.99",
  "was_price": null,
  "on_sale": false,
  "currency": "£",
  "rating": 4.8,
  "reviews_count": 9,
  "out_of_stock": false,
  "image": "https://media.johnlewiscontent.com/i/JohnLewis/113397136?",
  "images": [
    "https://media.johnlewiscontent.com/i/JohnLewis/113397136alt1"
  ],
  "colours": [
    "Black"
  ],
  "colour_count": 1,
  "parse_path": "next_data",
  "search_term": "laptop"
}
```

### Use cases

**Price benchmarking against a UK anchor.** Pull a category, join on brand and title against your own range, and you have John Lewis's shelf price next to yours. Because `was_price` and `on_sale` come from the site's own reduction history, you can separate a genuine markdown from a product that has simply always been priced there, which is the distinction that decides whether you follow the price down.

**Watching a category for markdowns.** Sort by `discount` and schedule the run. The rows come back ordered by how deeply the site has cut them, so the first page is the answer rather than something you compute afterwards. `was_price` gives you the depth of the cut in pounds.

**Stock and assortment monitoring.** `out_of_stock` is a boolean straight from the payload, so a daily run over a category tells you what John Lewis has stopped being able to supply. For a supplier, a competitor's stockout is the most actionable single signal there is, and it is invisible from the front end unless you open every product.

**Feeding a product database.** `product_id` and `sku_id` are stable identifiers, `brand` is a clean field rather than a substring of the title, and the colour variants come as a list. That is enough to build a catalogue without a normalization pass, and enough to diff yesterday's run against today's on identity rather than on fuzzy title matching.

### How it compares

| | this actor | `sian.agency/currys-product-scraper` | `apify/e-commerce-scraping-tool` |
|---|---|---|---|
| Per 1.000 products | **$4,50** | $8,50 | $6,00 |
| Actor-start fee | **none** | **$0,05 per run** | $0,0001 |
| Previous price / reduction history | **yes** | no | no |
| Rating and review count | **yes** | not stated | no |
| Stock flag | **yes** | not stated | no |
| Colour variants | **yes, named** | no | no |
| Covers John Lewis | **yes** | no | generic, needs configuring |

Honest about the other side: `apify/e-commerce-scraping-tool` has 575 monthly users and 49 reviews, and it will point at almost any shop rather than only this one. If you need many retailers and shallow fields, it is the better tool. This one goes deep on a single retailer.

### Pricing

One event. `product` costs $0,0045 per product returned, which is $4,50 per 1.000. All pricing is pay-per-event, so you only pay for products you actually receive. No actor-start fee, no per-compute-unit charges, and a run that returns nothing costs nothing.

### Limits and gotchas

- **A brand-only search term redirects.** "dyson" sends you to a brand landing page with no product grid. The run reports `redirected_to_content_page` with the destination URL. Open it, click through to the listing you want, and pass that URL in `url`.
- **John Lewis never returns an empty search.** A nonsense term still comes back with a few hundred unrelated products and nothing in the payload flags it. The run summary always reports `totalResults`, so check it looks sane for what you asked.
- **Requests egress from the UK**, because the site serves a different experience to visitors it reads as overseas. Prices are always in pounds.
- **`rating` and `reviews_count` are empty for products nobody has reviewed yet**, which is normal on newly listed lines. Roughly six in ten products in a typical search carry a rating.
- **`was_price` only appears when the site records a reduction.** A product that has never been discounted has `on_sale: false` and a null previous price, which is correct rather than missing.
- **480 products is the cap per run.** The site pages 24 at a time and most searches have fewer than 20 pages anyway; the run summary reports `pagesAvailable` so you know whether you hit the end of the listing or the end of your limit.

### FAQ

**Can I scrape John Lewis without an API key?**
Yes. The site publishes its listing data to anonymous visitors as JSON inside the page, and that is what this actor reads. There is no credential field in the input.

**Why does searching for a brand return nothing?**
John Lewis redirects brand terms to a curated brand page that has no product grid on it. The actor detects the redirect and returns the URL it landed on, so you can pick the real listing and pass it in `url`.

**Can I get the previous price before a discount?**
Yes, as `was_price`, taken from the reduction history in the site's own payload. Most John Lewis scrapers only read the rendered card, where that number does not appear.

**Does this work on category pages as well as searches?**
Yes. Paste any johnlewis.com listing URL into `url` and it is read the same way, including sort parameters you have already applied in the browser.

**How do I track price changes over time?**
Schedule the run and store `product_id`, `price` and the timestamp. The product id is stable, so a diff between two runs is an exact join rather than a title match.

### Related Actors

- [Price Scraper](https://apify.com/s-r/price-scraper---extract-prices-availability-from-any-url) — price and availability from any product URL
- [Google Shopping Scraper](https://apify.com/s-r/free-google-shopping-scraper---extract-offers-from-any-ean-sku) — offers from every merchant for an EAN or SKU
- [Amazon Bestsellers Scraper](https://apify.com/s-r/amazon-bestsellers) — ranked products across 15 Amazon marketplaces

# Actor input Schema

## `search_term` (type: `string`):

What to search for on johnlewis.com. Brand-only terms such as 'dyson' get redirected by the site to a brand landing page that has no product grid; when that happens the run tells you so and hands back the URL it landed on.

## `url` (type: `string`):

A johnlewis.com search or category URL to read instead of a search term. Paste it straight from your browser. Takes precedence over the search term.

## `limit` (type: `integer`):

How many products to return, 1 to 240. A John Lewis search caps at 48 products in practice: the payload advertises 13-15 pages but page 3 returns 404. Category URLs pasted into the url field may go deeper.

## `sort_by` (type: `string`):

Uses the site's own sort options.

## `retries` (type: `integer`):

Retry attempts per page, each with a rotated user agent and TLS fingerprint.

## Actor input object example

```json
{
  "search_term": "laptop",
  "url": "https://www.johnlewis.com/search?search-term=coffee+machine",
  "limit": 48,
  "sort_by": "relevance",
  "retries": 3
}
```

# Actor output Schema

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

One row per product in listing order.

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

itemCount, totalResults, pagesAvailable, pagesFetched, parsePath and the final URL.

## `errors` (type: `string`):

Failures with a code and a redacted message. Absent when the run had none.

# 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 = {
    "search_term": "laptop",
    "limit": 48,
    "retries": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/john-lewis-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 = {
    "search_term": "laptop",
    "limit": 48,
    "retries": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/john-lewis-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 '{
  "search_term": "laptop",
  "limit": 48,
  "retries": 3
}' |
apify call s-r/john-lewis-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/john-lewis-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/ppLYp38Wdbdahamn2/builds/uYINSFnnoURhMkYq8/openapi.json
