# Wine-Searcher Scraper | Wine Prices, Scores, Offers & LWIN (`abotapi/wine-searcher-scraper`) Actor

Look up wines on wine-searcher.com by name, URL, or LWIN code. Returns 30+ fields, including critic scores, prices, grape, region, appellation, producer, label image, user ratings, food pairing, live offer counts, cheapest merchant offer, and optional critic review breakdown.

- **URL**: https://apify.com/abotapi/wine-searcher-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Stats:** 29 total users, 5 monthly users, 96.8% runs succeeded, 2 bookmarks
- **User rating**: No ratings yet

## Pricing

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

## Wine-Searcher Scraper: Scores, Prices & Popularity

Look up any wine on Wine-Searcher and get a clean, structured record back. Give the actor a list of wine names, Wine-Searcher links, or LWIN codes and it returns critic scores, average and market prices, grape, region, appellation, producer, label image, user ratings and live offer counts as JSON or CSV. Built for merchants, collectors, sommeliers and market analysts who need Wine-Searcher data in a spreadsheet or pipeline instead of a browser tab.

### Why this scraper

- Three input styles in one actor: wine names, Wine-Searcher URLs, or LWIN codes (LWIN7 / 11 / 16 / 18), with automatic detection so you can mix them.
- Richer output than a plain price lookup: 30+ fields per wine, including user ratings, food pairing, region and origin, that other tools drop.
- Cheapest live offer with the merchant name when you enable offer extraction, plus the total number of offers worldwide.
- Optional per-critic review breakdown (reviewer name and score) when you enable review collection.
- Looks up several wines in parallel, over several connections at once on long lists, so a few-hundred-wine list finishes in minutes rather than hours.
- Long lists keep going: the actor re-establishes its connection as often as it needs to, and a wine that could not be read on one connection is retried on the next instead of being abandoned with the rest of the list.
- Pay only for wines actually found. Not-found and unavailable items are flagged separately and do not count.
- Currency context follows the proxy country (France / EUR by default), so prices match the market you care about.
- Optional one-click export of every result into Notion, Linear, Airtable or Apify through MCP connectors.

### Data you get

> Sample shape, values are illustrative placeholders, not from a live listing.

| Field | Example |
|---|---|
| `wineName` | `2015 Sample Wine, Pomerol, France` |
| `vintage` | `2015` |
| `appellation` | `Pomerol, France` |
| `region` | `Pomerol` |
| `grapeVariety` | `Merlot` |
| `style` | `Red - Savoury and Classic` |
| `producer` | `Sample Producer` |
| `description` | `Short producer / wine description text.` |
| `score` | `97` |
| `scoreBestRating` | `100` |
| `criticReviewsCount` | `20` |
| `criticReviews` | `[{ "author": "Sample Critic", "score": 95, "bestRating": 100, "tastingNote": "..." }]` |
| `avgPrice` | `5076` |
| `avgPriceCurrency` | `EUR` |
| `cheapestPriceAmount` | `3400.00` |
| `cheapestPriceCurrency` | `EUR` |
| `cheapestPriceMerchant` | `Sample Merchant` |
| `cheapestPriceMerchantUrl` | `https://www.example.com/wine/0000` |
| `bottlesPerUnit` | `1` |
| `offers` | `[{ "price": 3400.0, "priceCurrency": "EUR", "merchant": "Sample Merchant", "merchantUrl": "...", "unitDescription": "Bottle (750ml)", "bottlesPerUnit": 1, "availability": "InStock" }]` |
| `offersCount` | `13` |
| `highestPriceAmount` | `4500.00` |
| `highestPriceCurrency` | `EUR` |
| `medianPriceAmount` | `3800.00` |
| `merchantCount` | `11` |
| `labelImageUrl` | `https://www.wine-searcher.com/images/labels/00/00/000000000.jpg` |
| `wineSearcherUrl` | `https://www.wine-searcher.com/find/sample/2015` |
| `searchLocation` | `France` |
| `scrapedAt` | `2026-01-01T00:00:00.000Z` |

The full record, including the complete `offers` array (every merchant, price and link) and the `criticReviews` breakdown with tasting notes, is always kept in the Apify dataset.

### Specials, was-price & discount

Wine-Searcher is a price-comparison site, not a retail store. It lists merchant offers at face value and does **not** have a specials, deals, or clearance taxonomy, nor a was/original/list price or discount field on any offer. The price spread between merchants is **not** a discount: it reflects different merchants, bottle sizes, vintages, and stock conditions.

No `wasPrice`, `savingsAmount`, `savingsPercent`, `isOnSpecial`, or `specialsCategory` field is emitted, because none exists on the site. Wine-Searcher's "best value" and critic scores are **ratings**, not discounts.

Instead, the actor ships merchant-price-spread statistics computed from the already-fetched offers list:

| Field | Description |
|---|---|
| `cheapestPriceAmount` | Lowest offer price **exactly as a merchant lists it** (long-standing field, meaning unchanged). |
| `highestPriceAmount` | Highest price **per standard 750ml bottle** (see the basis note below). |
| `highestPriceCurrency` | Currency of that highest per-bottle price. |
| `medianPriceAmount` | Median price **per standard 750ml bottle**, same basis as `highestPriceAmount`. |
| `merchantCount` | Number of distinct merchants offering this wine. |
| `offersCount` | Total number of merchant offers (already shipped). |

**Basis note (important).** A wine's offers mix formats: single bottles, magnums, half bottles, and multi-bottle cases, and a case offer's listed price is the **case total**. Comparing those numbers at face value is meaningless, so `highestPriceAmount` and `medianPriceAmount` are normalised to **one standard 750ml bottle**: a case is divided by its bottle count (a case of 6 listed at 30,204 counts as 5,034 per bottle, not 30,204), and non-750ml formats such as magnums are left out of the comparison entirely. These two fields are therefore a like-for-like per-bottle comparison, **not necessarily a price you can click and buy**. If a wine has no standard-bottle offer at all, both are `null`. `cheapestPriceAmount` is untouched and keeps its original meaning (the lowest listed offer price).

These are price-spread fields for comparison across merchants, **not** discount fields. They are populated when `fetchOffers` is on and `null` otherwise.

### How to use

Look up by wine name (auto-detected):

```json
{
  "inputType": "auto",
  "wineNames": ["Petrus 2015", "Dom Perignon 2013"],
  "maxItems": 20
}
```

Look up by LWIN code:

```json
{
  "inputType": "lwins",
  "lwins": ["11316442021", "11084042019"]
}
```

Look up by Wine-Searcher URL, fastest mode (no offer extraction):

```json
{
  "inputType": "urls",
  "urls": ["https://www.wine-searcher.com/find/sassicaia/2019"],
  "fetchOffers": false
}
```

Pin a different currency market (United Kingdom / GBP):

```json
{
  "inputType": "auto",
  "wineNames": ["Sassicaia 2019"],
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "GB" }
}
```

### Resume & recurring tracking

**Resume from a previous run** (`resumeFromRunId`) is a one-off delta: give it a previous run or dataset ID and this run skips wines already collected there, looking up only the rest of your list. Useful for finishing a large list across several runs without re-paying for wines you already have.

**Incremental mode** (`incrementalMode`) is for a wine list you run on a schedule (daily/weekly) to track changes over time - critic score moved, price shifted, a merchant added or dropped an offer. Every wine is classified:

| `changeType` | Meaning |
|---|---|
| `NEW` | Never seen under this State key before. |
| `UPDATED` | Something changed since last time - `changedFields` names which top-level fields. |
| `UNCHANGED` | Identical to last time. Suppressed (not pushed, not billed) unless `emitUnchanged` is on. |
| `REAPPEARED` | Was `EXPIRED`, now found again. |
| `EXPIRED` | Tracked before, not found this run. Only reported when `emitExpired` is on AND this run completed a full scan of your list (no Resume, no cap, no connection ceiling cut it short). |

State is kept in a separate key-value store, keyed by `stateKey` (or an auto-derived hash of your exact wine list when left empty) - it never re-visits wine-searcher.com, so checking state costs no extra proxy/solver spend. A page-order-only difference in `offers`/`criticReviews` never counts as a change (see the code comments in `src/incremental.py` for what is and isn't proven about field volatility here).

⚠️ The auto-derived key is built from your wine list itself, so editing the list (adding or removing a wine) also changes the auto key and starts a fresh baseline - convenient if you always re-run the identical list, but it means `emitExpired` can never report a wine you removed as EXPIRED under the auto key. If you intend to grow/shrink your tracked list over time and still want removed wines reported, set your own fixed `stateKey` so every run shares one baseline regardless of list edits.

```json
{
  "inputType": "auto",
  "wineNames": ["Petrus 2015", "Opus One 2019"],
  "incrementalMode": true,
  "emitExpired": true
}
```

Long runs (large lists) also survive a platform migration or a manual Resurrect automatically: progress is checkpointed periodically, so a resumed attempt of the same run does not re-look-up (or re-charge for) wines it already extracted.

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

Optionally pipe every result into the tools you already use, no code required:

1. Authorize a connector under Apify, Settings, API & Integrations (Notion, Linear, Airtable or Apify).
2. Select it in the `mcpConnectors` input. For Notion, also set `notionParentPageUrl`.
3. Run the actor. Results are written to the connector after the dataset is saved.

The connector receives a condensed, human-readable summary per wine (a title plus the key fields). The complete record always stays in the Apify dataset. Leaving `mcpConnectors` empty skips this entirely and never changes the dataset output.

### Input parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `inputType` | string | `auto` | `auto`, `wineNames`, `urls`, or `lwins`. Auto-detect routes each item by its shape. |
| `wineNames` | array | (none) | Wine names, ideally with vintage year. Up to 1000. |
| `urls` | array | (none) | Wine-Searcher find URLs. Up to 1000. |
| `lwins` | array | (none) | LWIN codes (7 / 11 / 16 / 18 digit). Up to 1000. |
| `fetchOffers` | boolean | `false` | Also extract the cheapest offer, the full offers list and popularity. Slightly slower per wine. |
| `fetchReviews` | boolean | `false` | Also collect the per-critic review breakdown (reviewer name and score). |
| `concurrency` | integer | `3` | Deprecated and ignored. Lookup parallelism is managed automatically. |
| `maxItems` | integer | `20` | The single cap: process at most this many wines. `0` means no limit. |
| `proxy` | object | Residential FR | Residential proxy required. Country sets the price-currency context. |
| `mcpConnectors` | array | (none) | Optional MCP connectors to export results to. |
| `notionParentPageUrl` | string | (none) | Parent Notion page for the Notion export. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. |

### Output example

> Sample shape, values are illustrative placeholders, not from a live listing.

```json
{
  "inputValue": "Petrus 2015",
  "inputType": "wineNames",
  "wineSearcherUrl": "https://www.wine-searcher.com/find/sample/2015",
  "wineName": "2015 Sample Wine, Pomerol, France",
  "vintage": 2015,
  "appellation": "Pomerol, France",
  "region": "Pomerol",
  "grapeVariety": "Merlot",
  "style": "Red - Savoury and Classic",
  "producer": "Sample Producer",
  "description": "Short producer / wine description text.",
  "score": 97,
  "scoreBestRating": 100,
  "criticReviewsCount": 20,
  "criticReviews": [
    { "author": "Sample Critic", "score": 95, "bestRating": 100, "tastingNote": "Tasted: Jan 2018. ..." }
  ],
  "avgPrice": 5076,
  "avgPriceCurrency": "EUR",
  "cheapestPriceAmount": 3400.0,
  "cheapestPriceCurrency": "EUR",
  "cheapestPriceMerchant": "Sample Merchant",
  "cheapestPriceMerchantUrl": "https://www.example.com/wine/0000",
  "bottlesPerUnit": 1,
  "offers": [
    { "price": 3400.0, "priceCurrency": "EUR", "merchant": "Sample Merchant", "merchantUrl": "https://www.example.com/wine/0000", "unitDescription": "Bottle (750ml)", "bottlesPerUnit": 1, "availability": "InStock" }
  ],
  "offersCount": 13,
  "highestPriceAmount": 4500.0,
  "highestPriceCurrency": "EUR",
  "medianPriceAmount": 3800.0,
  "merchantCount": 11,
  "labelImageUrl": "https://www.wine-searcher.com/images/labels/00/00/000000000.jpg",
  "searchLocation": "France",
  "scrapedAt": "2026-01-01T00:00:00.000Z"
}
```

### Long lists

There is no separate setting for this: set `maxItems` to the size of your list and the actor sizes
itself. It reads a batch of wines per connection, then opens a fresh one and carries on, running up
to three connections at once once it has confirmed the site is reachable. A run that genuinely
cannot reach the site still stops within a few seconds and reports a connection problem rather than
an empty result, so a total block never looks like "your list matched nothing".

### Plan requirement

Wine-Searcher only accepts residential connections, so this actor needs Apify Residential proxy (Starter plan or higher). On a free plan without residential access the run will return no data. The default pins France for EUR pricing; change the proxy country to switch the price-currency market. Critic scores and average price are always included; the full offers list and popularity rank are included when you enable offer fetching, and the per-critic review breakdown with tasting notes is included when you enable review collection.

# Actor input Schema

## `inputType` (type: `string`):

How the items below are formatted. 'Auto-detect' inspects each item and routes it automatically (a wine-searcher.com URL, an all-digit LWIN code, or otherwise a wine name).

## `wineNames` (type: `array`):

Full wine names, ideally with vintage year, e.g. 'Domaine Leflaive Puligny-Montrachet Les Pucelles 2020'. Up to 1000 items.

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

Direct wine-searcher.com/find/... links. Up to 1000 items.

## `lwins` (type: `array`):

Liv-ex Wine Identification Numbers (LWIN7, LWIN11, LWIN16, LWIN18), numeric or string. Up to 1000 items.

## `fetchOffers` (type: `boolean`):

When on, the actor also extracts the cheapest merchant offer (price + merchant), the full offers list and the popularity rank. Adds a little time per wine. Leave off for a faster, cheaper run that still returns scores, average price, grape, region and the offer count.

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

When on, the actor also collects the per-critic review breakdown (reviewer name and score) for each wine. Each review captured is one enriched data item.

## `concurrency` (type: `integer`):

Deprecated and ignored. Lookup parallelism is managed automatically.

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

The single cap on this run: process at most this many wines from your list. Set to 0 for no limit (process the whole list). Keeps a first run cheap and predictable.

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

Residential proxy is required. The country also determines which regional currency the average price is shown in (default France / EUR).

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

Optional. ID of a previous run of this actor (or a dataset ID). Wines already collected there are skipped, so this run only looks up the rest of your list. Max wines then counts only the new wines. One-off continuation of a SPECIFIC run - for ongoing scheduled tracking of the same wine list use Incremental mode below instead.

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

For a wine list you run on a schedule (e.g. daily/weekly): remembers what this exact list returned last time and classifies every wine as NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED, so you can act on only what changed (score moved, price shifted, a merchant appeared or vanished). Off by default so existing scheduled runs are unaffected. Uses State key below to isolate different lists; see Emit unchanged rows / Emit expired rows to control which of those get pushed (both bill extra rows when on).

## `stateKey` (type: `string`):

Names the saved baseline Incremental mode compares against. Leave empty to auto-derive one from your exact wine list (Input type + wineNames/urls/lwins) - convenient when you re-run the SAME list every time, but editing the list also changes this auto key, starting a fresh baseline. If you plan to add or remove wines over time and still want EXPIRED reported for ones you drop, set your own fixed text here so every run shares one baseline regardless of list edits. Ignored when Incremental mode is off.

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

When Incremental mode is on: also push rows classified UNCHANGED (identical to last run), not just NEW/UPDATED/REAPPEARED/EXPIRED. Off by default to save you from paying for rows that didn't change. Turning this on bills the per-result event (and the detail-enrichment surcharge, when fetched) for every unchanged row too.

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

When Incremental mode is on: push a synthetic EXPIRED row for wines tracked in a previous run that this run could not find. Only fires after a run that completely covered your whole tracked list: if Resume from run/dataset ID was used, if the run stopped early (a connection/session ceiling), or if Max wines cut the list short, expiry reporting is skipped for that run (with a log line saying why) because a partial scan can't tell 'gone' apart from 'not reached yet'. Off by default; turning it on bills the per-result event for each EXPIRED row emitted. EXPIRED rows count against Max wines like any other row; if the cap is reached, the remaining gone wines stay tracked and are reported on a later run rather than being emitted uncapped.

## `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 rich page-per-item export; other connectors get a best-effort write/digest. Leave empty to skip; 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 item pages are created. Required to enable the Notion export; ignored by other connectors.

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

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

## Actor input object example

```json
{
  "inputType": "auto",
  "wineNames": [
    "Domaine Leflaive Puligny-Montrachet Les Pucelles 2020",
    "Petrus 2015"
  ],
  "urls": [
    "https://www.wine-searcher.com/find/petrus/2015"
  ],
  "lwins": [
    "11316442021",
    "11084042019",
    "1131644"
  ],
  "fetchOffers": false,
  "fetchReviews": false,
  "concurrency": 3,
  "maxItems": 20,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "FR"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# 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 = {
    "inputType": "auto",
    "wineNames": [
        "Domaine Leflaive Puligny-Montrachet Les Pucelles 2020",
        "Petrus 2015"
    ],
    "urls": [
        "https://www.wine-searcher.com/find/petrus/2015"
    ],
    "lwins": [
        "11316442021",
        "11084042019",
        "1131644"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "FR"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/wine-searcher-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 = {
    "inputType": "auto",
    "wineNames": [
        "Domaine Leflaive Puligny-Montrachet Les Pucelles 2020",
        "Petrus 2015",
    ],
    "urls": ["https://www.wine-searcher.com/find/petrus/2015"],
    "lwins": [
        "11316442021",
        "11084042019",
        "1131644",
    ],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "FR",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/wine-searcher-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 '{
  "inputType": "auto",
  "wineNames": [
    "Domaine Leflaive Puligny-Montrachet Les Pucelles 2020",
    "Petrus 2015"
  ],
  "urls": [
    "https://www.wine-searcher.com/find/petrus/2015"
  ],
  "lwins": [
    "11316442021",
    "11084042019",
    "1131644"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "FR"
  }
}' |
apify call abotapi/wine-searcher-scraper --silent --output-dataset

```

## MCP server setup

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