# Laithwaites Wine Scraper (`crawlerbros/laithwaites-wine-scraper`) Actor

Scrape Laithwaites Wine (laithwaites.co.uk), a leading UK wine retailer. Browse by category (colour, style, grape, region, offer) with full pagination, or fetch specific products by URL. Get price, vintage, origin, ABV, rating, review count, and stock.

- **URL**: https://apify.com/crawlerbros/laithwaites-wine-scraper.md
- **Developed by:** [Crawler Bros](https://apify.com/crawlerbros) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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

## Laithwaites Wine Scraper

Scrape [Laithwaites Wine](https://www.laithwaites.co.uk) — a leading UK
wine retailer and wine club. Browse by category (colour, style, grape,
region, or offer) with full pagination, or fetch specific products by
URL/item code. Get price per bottle, vintage, grape, country/region,
ABV, customer rating, review count, and stock status. No login, no
cookies, no paid proxy required.

### What this actor does

- **Browse by category** — 65 real categories (colour: red/white/rosé/
  sparkling; style: light-juicy-reds, crisp-fresh-whites, etc.; grape:
  Cabernet Sauvignon, Chardonnay, Malbec, etc.; region: France, Italy,
  Bordeaux, Rioja, etc.; curated: 5-star-rated, award-winning, on-offer,
  bin ends) — with real server-side pagination (up to the category's full
  result count).
- **Fetch specific products** — supply product URLs or bare item codes
  to pull full details (tasting notes, accolades, allergens, dietary
  flags) for known wines.
- **Filter results** — price range, minimum rating, in-stock only, wine
  type, country of origin, keyword.
- **Sort results** (`byCategory`) — relevance, top rated, price low-to-high/
  high-to-low, or name A-Z/Z-A, matching laithwaites.co.uk's own sort
  control.
- **Empty fields are omitted** — only fields Laithwaites actually
  publishes for a given wine are included; no null/placeholder values.

### Output fields

Every record always includes `itemCode`, `title`, `productUrl`,
`sourceUrl`, `recordType`, and `scrapedAt`. Other fields are included
only when Laithwaites actually provides them for that wine (no
null/placeholder values are ever emitted):

| Field | Description |
|---|---|
| `itemCode` | Laithwaites' internal product code |
| `title` / `webHeadline` | Product name and marketing headline |
| `description` | Full product description — `byProductUrls` only |
| `longDescription` | Extended marketing copy (backstory, winemaker, tasting context) — `byProductUrls` only, only when it's genuinely different/longer content than `description` |
| `pricePerBottle` / `currency` | Lowest available per-bottle price (GBP) |
| `rrpPricePerBottle` | Original RRP per bottle, when the sale price is a discount off list price |
| `memberPricePerBottle` | Laithwaites "Wine Plan" member-exclusive per-bottle price, when it's lower than the standard price |
| `vintage` | Vintage year (or descriptive text for non-standard vintages) |
| `wineType` / `colour` | Red / White / Rosé / Sparkling / Dessert / Fortified |
| `productType` / `productWebType` | Laithwaites' own product classification tags — `byProductUrls` only |
| `wineStyle` | Laithwaites' curated style tag (e.g. "Ripe Smooth Reds") |
| `style` | Body/style descriptor (e.g. "Red - Medium to Full Bodied") |
| `grape` | Grape variety/blend |
| `country` / `region` / `appellation` | Origin |
| `bottleSize` / `bottleVolumeMl` / `numberOfBottles` / `packaging` | Format (bottle size, volume, bottles per case, packaging type) |
| `abvBand` | ABV band (category listing) |
| `abv` | Exact ABV % — `byProductUrls` only |
| `tastingNotesAroma` / `tastingNotesColor` / `tastingNotesTaste` | Tasting notes — `byProductUrls` only |
| `averageRating` / `reviewCount` | Customer rating out of 5 and review count |
| `inStock` / `stockQuantity` / `availabilityStatus` / `availabilityDate` | Availability, and pre-order date when not yet in stock |
| `dietary` / `allergens` | Dietary flags (Vegan, Vegetarian) / allergen declarations |
| `awardWinner` / `fineWine` / `organic` / `biodynamic` / `vegan` / `vegetarian` / `kosher` / `enPrimeur` / `giftFlag` / `flexiFlag` / `preSell` / `memberPriceAvailable` | Product flags |
| `offers` | Active promotional offer tags, e.g. `"On Offer"`, `"Mix and Match"` (category listing) |
| `accolades` | Critic/award accolades — `byProductUrls` only |
| `tastingNotesColor` | Colour tasting note (category listing; full aroma/colour/taste triple on `byProductUrls`) |
| `priceRangeLabel` | Laithwaites' own price-band label (category listing) |
| `bottleType` | Bottle material, e.g. "Glass" — `byProductUrls` only |
| `drinkByDate` | Recommended drink-by date, when Laithwaites publishes one — `byProductUrls` only |
| `alcoholUnits` | UK alcohol units per bottle — `byProductUrls` only |
| `popularityScore` | Laithwaites' own popularity ranking signal (category listing) |
| `categoryTags` | Every taxonomy tag this wine is filed under on laithwaites.co.uk (region, style, offers, occasions, etc.) — category listing |
| `imageUrl` | Product image — `byProductUrls` only |
| `searchCategory` | Echo of the category searched |

### Modes

#### Browse by category (`byCategory`, default)

Laithwaites' own category pages support real pagination (24 results per
page); the actor pages through automatically up to `maxItems` or the
category's full result count, whichever is smaller, honouring the site's
`Crawl-delay: 5` from robots.txt between requests.

Set `sortBy` to reorder results the same way laithwaites.co.uk's own sort
control does (`relevance` / `ratingDesc` / `priceAsc` / `priceDesc` /
`nameAsc` / `nameDesc`). There's no reachable server-side sort endpoint,
so a non-default `sortBy` makes the actor collect a larger candidate pool
(up to 300 results) from the category's real pages first, then sorts
locally before applying `maxItems`.

#### Fetch specific products (`byProductUrls`)

Supply full product URLs or bare item codes to pull complete detail
(tasting notes, accolades, allergens, dietary flags) for known wines.

### Filters

- `priceMin` / `priceMax` — GBP per-bottle price range (records without a
  price always pass).
- `minRating` — minimum average customer rating, 0-5 scale (records
  without a rating always pass).
- `inStockOnly` — only in-stock wines.
- `wineType` — Red / White / Rosé / Sparkling / Dessert / Fortified.
- `country` — case-insensitive substring match on country of origin.
- `keyword` — case-insensitive substring match against title, headline,
  grape, region, and appellation.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | string | `byCategory` | `byCategory` / `byProductUrls` |
| `category` | string | `red-wine` | 65 real categories (colour, style, grape, region, curated) (mode=byCategory) |
| `productUrls` | array | `[]` | Product URLs or bare item codes to fetch directly (mode=byProductUrls, required) |
| `sortBy` | string | `relevance` | `relevance` / `ratingDesc` / `priceAsc` / `priceDesc` / `nameAsc` / `nameDesc` (mode=byCategory) |
| `priceMin` / `priceMax` | integer | `0` | GBP per-bottle price range; records without a price always pass |
| `minRating` | integer | `0` | Minimum average customer rating, 0-5; records without a rating always pass |
| `inStockOnly` | boolean | `false` | Only include wines currently in stock |
| `wineType` | string | `""` (any) | `Red` / `White` / `Rose` / `Sparkling` / `Dessert` / `Fortified` |
| `country` | string | – | Substring match on country of origin |
| `keyword` | string | – | Substring match on title, headline, grape, region, appellation |
| `maxItems` | integer | `24` | Hard cap on emitted records (1-300) |

#### Example: browse red wines under £15

```json
{
  "mode": "byCategory",
  "category": "red-wine",
  "priceMax": 15,
  "maxItems": 24
}
```

#### Example: top-rated Bordeaux, in stock only

```json
{
  "mode": "byCategory",
  "category": "bordeaux-wine",
  "sortBy": "ratingDesc",
  "inStockOnly": true
}
```

#### Example: fetch specific products by URL/item code

```json
{
  "mode": "byProductUrls",
  "productUrls": ["https://www.laithwaites.co.uk/product/0022625", "0022625"]
}
```

### Use cases

- **Wine retailers/competitors** — track Laithwaites' pricing and offers across categories
- **Price comparison sites** — build a dataset of per-bottle prices, vintages, and ratings
- **Sommeliers/enthusiasts** — filter wines by region, grape, and rating to build shortlists
- **Gifting/e-commerce apps** — pull product detail (tasting notes, ABV, allergens) for known wines
- **Market research** — monitor stock status and promotional offers over time

### FAQs

**Do I need to provide cookies, login, or a proxy?**
No. The actor works against Laithwaites' own public, server-rendered
pages with zero-cost, proxy-free requests. A free Apify datacenter proxy
is engaged automatically only as a fallback if repeated requests are
temporarily blocked.

**Why does opening a `productUrl`/`sourceUrl` with plain `curl` return a 403?**
Laithwaites' bot protection blocks generic off-platform requests while
the identical URL loads normally in a real browser and in this actor's
own Apify cloud runs. The URLs themselves are always genuine, live
product pages; a 403 from a manual `curl` check off-platform is expected
and does not indicate a broken link.

**Is this affiliated with Laithwaites?**
No, this is a third-party actor built against laithwaites.co.uk's public
pages; it is not affiliated with or endorsed by Laithwaites.

**Why do some records have fewer fields than others?**
Only fields Laithwaites actually publishes for a given wine are included
— the actor never fabricates placeholder values. Fields like tasting
notes, accolades, and allergens are only available from the product
detail page (`byProductUrls` mode); category-listing records carry a
lighter field set for speed.

**Why might the price differ between category-listing and
`byProductUrls` mode for the same wine?**
Category listings show the lowest available per-bottle price (which can
include promotional case-mix pricing); the product detail page shows the
standard single-bottle price. Both are real, current prices from
Laithwaites — just different pricing tiers.

**Does this actor respect robots.txt?**
Yes. Category browsing (`/wine/<slug>`) and product-detail (`/product/ <itemCode>`) pages used by this actor are not disallowed in
laithwaites.co.uk's robots.txt. The legacy `/wines` and `/search` paths
ARE disallowed and are intentionally not used. The actor also honours
the site's requested `Crawl-delay: 5` between requests.

### Limitations

- The category-listing API response also carries a handful of internal
  merchandising/engineering fields with no customer-facing meaning
  (e.g. an internal SKU family code, a numeric merchandise-line-type
  enum, and duplicate/rounded copies of fields already exposed more
  precisely elsewhere, like a plain integer star rating that duplicates
  the more precise `averageRating`) — these are intentionally not
  mapped to output fields since they carry no usable signal for a data
  consumer.

# Actor input Schema

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

byCategory: browse a category (colour/style/grape/region/offer) with full pagination. byProductUrls: fetch specific products directly by URL or bare item code.

## `category` (type: `string`):

Category to browse. Required for mode=byCategory.

## `productUrls` (type: `array`):

Full laithwaites.co.uk/product/... URLs, or bare item codes. Required for mode=byProductUrls.

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

Order results the same way laithwaites.co.uk's own sort control does. There is no server-side sort endpoint, so this actor collects a larger candidate pool from the category's real pages and sorts it locally before applying `maxItems`.

## `priceMin` (type: `integer`):

Only include wines priced at or above this amount (GBP per bottle). Records missing a price always pass.

## `priceMax` (type: `integer`):

Only include wines priced at or below this amount (GBP per bottle). Records missing a price always pass.

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

Only include wines with an average customer rating at or above this value. Records missing a rating always pass.

## `inStockOnly` (type: `boolean`):

Only include wines currently in stock.

## `wineType` (type: `string`):

Only include wines of this type. Records missing a type always pass.

## `country` (type: `string`):

Only include wines whose country of origin contains this text (case-insensitive, e.g. "France", "Australia"). Records missing a country always pass.

## `keyword` (type: `string`):

Only include wines whose title, headline, grape, region, or appellation contain this text (case-insensitive).

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

Maximum number of wine records to return.

## Actor input object example

```json
{
  "mode": "byCategory",
  "category": "red-wine",
  "productUrls": [],
  "sortBy": "relevance",
  "priceMin": 0,
  "priceMax": 0,
  "minRating": 0,
  "inStockOnly": false,
  "wineType": "",
  "country": "",
  "keyword": "",
  "maxItems": 24
}
```

# Actor output Schema

## `wines` (type: `string`):

Dataset containing all scraped wine records.

# 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": "byCategory",
    "category": "red-wine",
    "productUrls": [],
    "sortBy": "relevance",
    "priceMin": 0,
    "priceMax": 0,
    "minRating": 0,
    "inStockOnly": false,
    "wineType": "",
    "country": "",
    "keyword": "",
    "maxItems": 24
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawlerbros/laithwaites-wine-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": "byCategory",
    "category": "red-wine",
    "productUrls": [],
    "sortBy": "relevance",
    "priceMin": 0,
    "priceMax": 0,
    "minRating": 0,
    "inStockOnly": False,
    "wineType": "",
    "country": "",
    "keyword": "",
    "maxItems": 24,
}

# Run the Actor and wait for it to finish
run = client.actor("crawlerbros/laithwaites-wine-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": "byCategory",
  "category": "red-wine",
  "productUrls": [],
  "sortBy": "relevance",
  "priceMin": 0,
  "priceMax": 0,
  "minRating": 0,
  "inStockOnly": false,
  "wineType": "",
  "country": "",
  "keyword": "",
  "maxItems": 24
}' |
apify call crawlerbros/laithwaites-wine-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,crawlerbros/laithwaites-wine-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/wLnqukXKhbAGtbCdL/builds/PSsD4KqlPwGQcF5hQ/openapi.json
