# LOOKFANTASTIC Product & Sale Price Scraper (UK) (`superslowsloth/lookfantastic-products`) Actor

LOOKFANTASTIC scraper: search terms or category, brand and offer pages to one row per product with GBP price, RRP, discount percent, promo text and voucher code, brand, size variant, stock and image. For UK beauty sale and promo price watching.

- **URL**: https://apify.com/superslowsloth/lookfantastic-products.md
- **Developed by:** [Superslow Sloth](https://apify.com/superslowsloth) (community)
- **Categories:** E-commerce, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 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?

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

## LOOKFANTASTIC Product & Sale Price Scraper (UK)

Search [LOOKFANTASTIC](https://www.lookfantastic.com) (a UK beauty retailer) by
keyword, or read a category, brand or offer page, and get one flat row per
product: the current price and the **RRP in GBP**, the discount in pounds and
percent, the promo text and its **voucher code**, brand, size variant, whether it
can be bought, the product link and the photo. Run it on a schedule and diff the
dataset to get a **LOOKFANTASTIC sale and promo price watch**: which serums,
brands or offers dropped, and which only advertise a code.

No account, no API key, no login, no browser. One request returns 32 products.

### Input

| Field | Notes |
|---|---|
| `searchTerms` | One or more search terms, one per line (`serum`, `medik8`, `retinol night cream`). A term the site recognises is sent to its category or brand page, and that whole list is read; any other term is read as search results. A product found by two terms is returned once, under the first. |
| `startUrls` | LOOKFANTASTIC pages to read in full: a category (`/c/health-beauty/face/skincare-products/specific-care/serums/`), a brand (`/c/brands/the-ordinary/`) or an offer page (`/c/offers/offers/22/`). A search URL (`/search/?q=...`) is read as that search term. Pages must list products. A brand page is read through its own list of everything (see below); a category landing page such as `/c/health-beauty/face/` lists nothing and is refused. Give terms, URLs or both. |
| `maxItems` | Products to read per term or page (default 100, up to 10,000). 32 per page. |
| `onlyOnSale` | Return only products whose current price is below the RRP. Products that are not reduced are read, not returned and not charged. |
| `sort` | `popularity` (default), `price-asc`, `price-desc`, `name`, `newest`: the five choices of the site's own sort menu. |
| `proxyConfiguration` | Apify datacenter proxy by default. |

### Output

Prices are numbers in pounds; `currency` is the currency the page states (GBP on
every page measured).

| Field | Notes |
|---|---|
| `query`, `position` | The search term or page URL that first found the product, and its place in the site's list for that source and sort (1 is first). |
| `product_id`, `url` | LOOKFANTASTIC's product id (`12595875`; the site's "sku") and the product link. |
| `name`, `brand` | As the page shows them. `brand` is null if the page sends none. |
| `price`, `original_price` | `price` is the price now, the card's "Current price". `original_price` is the **RRP** the card strikes through. They are equal when the product is not reduced. |
| `on_sale`, `discount_amount`, `discount_percent` | `on_sale` is true when `price` is below `original_price`. `discount_amount` is the pounds off, as the page sends it. `discount_percent` is worked out from the two prices (one decimal place). Both are **null when the product is not on sale**, never 0. |
| `promo`, `promo_code` | The promotion banner on the card: `Save 20%`, `Save 22% \| Use Code: FLASH22`, `NEW IN`, `Complimentary Gift Set`, `Dermatologist Recommended`. `promo_code` is the voucher code the text names (`FLASH22`). Null when there is none. |
| `variant`, `variant_count` | The page's variant label and how many variants the product has. See the note below. |
| `in_stock`, `stock_label` | False with `stock_label` `COMING SOON` when the card shows a disabled button instead of "Quick buy". |
| `image_url` | The product photo, original size, on `static.thcdn.com`. |
| `list_name` | The list's own title in the site's tracking data (`Facial Serums`). Null on search pages. |

Example row (real values, 2026-10-15):

```json
{
  "query": "https://www.lookfantastic.com/c/health-beauty/face/skincare-products/specific-care/serums/",
  "position": 3,
  "product_id": "12595875",
  "name": "ESPA Optimal Skin Rejuvenating Night Booster 30ml",
  "brand": "ESPA",
  "url": "https://www.lookfantastic.com/p/espa-optimal-skin-rejuvenating-night-booster-30ml/12595875/",
  "image_url": "https://static.thcdn.com/productimg/original/12595875-1145339338587141.jpg",
  "currency": "GBP",
  "price": 28.8,
  "original_price": 48.0,
  "on_sale": true,
  "discount_amount": 19.2,
  "discount_percent": 40.0,
  "promo": "Save 40%",
  "promo_code": null,
  "variant": null,
  "variant_count": 1,
  "in_stock": true,
  "stock_label": null,
  "list_name": "Facial Serums"
}
```

#### What the price fields mean

The page's own tracking data calls the RRP `price` and the current price
`value`, which reads backwards. Which is which was checked on 160 product cards
(the struck-through figure on every card equals the RRP, the "Current price"
equals the current price) and on all 873 products of one category (the pounds
off equal the difference, with no exception).

**A voucher code is not a reduction.** Under a banner such as `Save 22% | Use
Code: FLASH22` the card still shows the full price: the 22% is taken at checkout.
`price` does not include it, `on_sale` stays false when the product is not
otherwise reduced, and the offer is in `promo` and `promo_code`. A product can be
reduced and carry a code too; its `price` is the reduced figure without the code.
Filter on `promo_code` to see code offers, on `on_sale` for shelf reductions.

#### Honest nulls

A field the page did not send is `null`, never `0` or `""`. A `COMING SOON`
product can have no price at all (the page sends null for both): its row is
still returned, with null `price` and `original_price`.

- **No rating or review count.** The ratings are filled in by the browser after
  the page loads and are not in the HTML, so the rows do not carry them.
- **`variant` is not the size of the row.** It is the page's label for the
  product's default variant. The `Clarins Double Serum 50ml` row says `30ml`, and
  so do its 75ml and 30ml siblings. Read the size from `name` and `url`.
- **`in_stock` is about the card.** True means a working buy button, false a
  disabled one. The site does not say more (stock levels, delivery dates).

### Pricing

Pay per event:

- **$0.002** per run (`actor-start`), charged only after your input is
  validated, so a run that fails on bad input costs nothing.
- **$0.00105** per product row written (`product-scraped`), i.e. $1.05 per
  1,000 products. A row that is not written is not charged: rows filtered by
  `onlyOnSale`, and repeats of a product another term already returned, are free.

### How it fetches

It reads the product list that lookfantastic.com's own pages embed (an item map
keyed by product id) and the product cards next to it. Measured 2026-10-15: HTTP
200 HTML from a plain home connection and from an Apify datacenter address, no key
or cookie needed. A refusal (403, 429, 5xx), a 200 that is not a LOOKFANTASTIC
page, or a dropped connection is treated as the address being blocked, and the
actor rotates to a fresh one and retries.

Things worth knowing, all measured:

- **A search does not always return search results.** `/search/?q=serum` is
  redirected to the Serums category (873 products), `numbuzin` to the Numbuzin
  brand page, `red lipstick` to a lipsticks list. A term the site does not
  recognise is answered in place (`retinol night cream`: 103 products, 4 pages),
  and one it cannot match at all gets no results. The actor reads whatever list
  the site lands on, all its pages. `query` stays the term you typed. A broad
  term can therefore return a whole category; `maxItems` bounds it.
- **A brand name lands on a brand page, not a list.** `cerave`, `medik8`, `nars`,
  `la roche posay`, `ordinary`, `elemis` and `ghd` are all redirected to a hub of
  banners and carousels that holds no product list. Each hub links the brand's
  list of everything (`/c/brands/cerave/view-all/`, `/c/brands/medik8/view-all-medik8/`,
  `/c/brands/elemis/all-elemis/`); the actor follows that link and reads it, all
  its pages, in the sort you chose (CeraVe 84 products, The Ordinary 99, Medik8
  69 on 2026-10-15). The same applies to a brand page given as a start URL. The
  run log says which list was read.
- **The redirect drops `sort`.** The actor re-reads the first page from the
  category when you asked for a sort, so the order you chose is the order you get.
- **The price sorts follow the site's own price index.** Over all 28 pages of
  one category the displayed current price ran in order on 27 pages in each
  direction; one page in each held one product out of strict order.
- **Depth.** The site lists 32 products per page and has no page-size setting.
  The deepest list measured has 134 pages; page 100 answered in full. A page past
  the last is an empty list and the actor stops there. At most 10,000 products
  per term or page.
- **Pages repeat nothing.** All 28 pages of one category were read: 873
  products, none twice. A guard still ends a walk after three pages in a row that
  bring nothing new.
- **Category landing pages have no list.** `/c/health-beauty/face/` is a page of
  carousels and links no list of everything below it; it fails (at no charge) with a
  message rather than return nothing. Use a deeper category such as
  `/c/health-beauty/face/skincare-products/`.
- **Not offered.** The site's facet filters (brand, price range, skin type) are
  not wired into this actor; start from a category or brand page that already
  narrows the list.

# Actor input Schema

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

What you would type into the LOOKFANTASTIC search box, one term per line (for example serum, medik8 or retinol night cream). Each term is read separately. A term the site recognises is sent to its category or brand page and that whole list is read (a brand's list of everything, for a brand name); any other term is read as search results. A product that two terms both find is returned once, under the first.

## `startUrls` (type: `array`):

LOOKFANTASTIC pages to read in full, for example https://www.lookfantastic.com/c/brands/the-ordinary/ or https://www.lookfantastic.com/c/health-beauty/face/skincare-products/specific-care/serums/. Use a page that lists products. A brand page (/c/brands/cerave/) is read through its own list of everything; a category landing page such as /c/health-beauty/face/ has none and is refused. A search URL (lookfantastic.com/search/?q=...) is read as that search term. Leave empty to use search terms only.

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

Read at most this many products for each search term or page. The site lists 32 per page, so 100 reads four pages. Up to 10,000 are accepted.

## `onlyOnSale` (type: `boolean`):

Return only products whose current price is below the RRP (the on_sale field). Products that are not reduced are read but not returned and not charged, and do not count toward Max products. A promo that needs a voucher code is not a reduced price; its row has on_sale false, so read the promo and promo_code fields for those.

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

The order LOOKFANTASTIC lists the products in, the same five choices as its own sort menu. 'Popularity' is its default. The price sorts follow the site's own price index, which can place a product slightly out of strict order.

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

Use Apify's automatic datacenter proxy (the default). lookfantastic.com answered an Apify datacenter address with HTTP 200 when this actor was built; the residential group was not needed. A refused request is retried from a fresh address.

## Actor input object example

```json
{
  "searchTerms": [
    "serum"
  ],
  "startUrls": [],
  "maxItems": 100,
  "onlyOnSale": false,
  "sort": "popularity",
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (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 = {
    "searchTerms": [
        "serum"
    ],
    "startUrls": [],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/lookfantastic-products").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 = {
    "searchTerms": ["serum"],
    "startUrls": [],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/lookfantastic-products").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 '{
  "searchTerms": [
    "serum"
  ],
  "startUrls": [],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call superslowsloth/lookfantastic-products --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/lookfantastic-products"
        }
    }
}
```

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/dYzSECrLeBRymVlfi/builds/WZk5B4cIhDXcYswQs/openapi.json
