# Waitrose Grocery Price & Offer Watch (UK) (`superslowsloth/waitrose-products`) Actor

Search Waitrose & Partners groceries by keyword and get one flat row per product: price in GBP, unit price (per kg or litre), was-price, every offer with its end date, rating, review count, categories, image and URL. Sort by price, rating or name. For weekly UK grocery price and offer tracking.

- **URL**: https://apify.com/superslowsloth/waitrose-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 $0.42 / 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

## Waitrose Grocery Price & Offer Watch (UK)

Search [Waitrose & Partners groceries](https://www.waitrose.com) for one or more
terms and get one flat row per product: the shelf price in pounds, the regular
price, the was-price when an offer lowers the price, the unit price (per litre,
kilo or each), **every offer with its wording and end date**, rating, review
count, categories, image and the product URL. Run it on a schedule and diff the
dataset to get a **UK grocery price-change and offer watch**, or read a whole
aisle for price comparison against Sainsbury's, Tesco, Asda and Aldi.

No account, no API key, no login, no browser. One request returns up to 96
products.

### Input

| Field | Notes |
|---|---|
| `searchTerms` | One or more search terms, one per line. Required. A product found by two terms is returned once, under the first. Waitrose matches loosely: `milk` finds 488 products, including milk drinks, chocolate and cheese. |
| `maxItems` | Products to read per search term (default 100, up to 10,000). |
| `sort` | `relevance` (default), `popular`, `price-asc`, `price-desc`, `rating`, `name-asc`, `name-desc`. |
| `onlyOffers` | Keep only products on an offer (Waitrose's "All Offers" filter). Default off. |
| `proxyConfiguration` | Apify proxy by default (see "How it fetches"). |

#### About the sorts

The sorts are Waitrose's own. The response says which sort it applied, and the
actor fails a term (at no charge) rather than return rows in an order that was
not asked for.

- **Sponsored products are pinned to page one.** Waitrose's paid placements
  appear at the top of the first page whatever the sort, so the first rows of a
  price sort can be out of order. They are flagged `is_sponsored`; drop them if
  you want a clean ranking.

### Output

Prices are numbers in pounds sterling; `currency` is always `GBP`.

| Field | Notes |
|---|---|
| `query` | The search term that first found this product. |
| `line_number`, `product_id`, `url` | Waitrose's six-digit product id (a string, leading zeros kept), the longer id the product page ends in, and the `https://www.waitrose.com/ecom/products/<slug>/<product_id>` link. |
| `name`, `brand`, `size` | As Waitrose shows them. `size` is the pack size as printed (`2litre`, `75cl`, `each`, `per kg`). Null when Waitrose sends none; `brand` is null for own-label lines that carry none. |
| `product_type` | `G` grocery, `W` wine, `E` a made-to-order or sold-by-weight line (whole fish, cakes). |
| `categories`, `tags`, `badges` | The category path from the root, shelf attributes (`Organic`, `British`) and marketing badges (`New lower price`). Empty lists when none. |
| `image_url`, `thumbnail_url` | The product photo, large and small. |
| `is_sponsored` | True when this row came from a paid sponsored slot. |
| `price` | The price on the shelf label with a **price-cutting offer applied** ("Save 65p", "20% Off", "New Lower Price"). A multi-buy ("Any 2 for £4") changes nothing here. |
| `regular_price` | The price with no offer applied. |
| `was_price` | The regular price, only when an offer puts `price` below it. **Null otherwise**, never 0. |
| `price_estimated`, `sale_unit`, `typical_weight_kg` | For goods sold by weight (`sale_unit` `kg` or `g`), `price` is the estimate for a pack of about `typical_weight_kg`; the final price depends on the weight picked. |
| `weight_range`, `price_range`, `lead_time_days` | Made-to-order lines: the ranges as printed (`(0.9kg-1.3kg)`, `£32-£46`) and the days of notice needed. |
| `unit_price`, `unit_measure`, `unit_price_text` | The price per `litre`, `kg`, `each`, `100 sheets` and so on, printed under the price (offer included). Null when the label is missing or in another shape; `unit_price_text` always keeps the label as sent. |
| `on_promotion`, `my_waitrose_offer` | `on_promotion` is true when an offer is shown on the shelf. A standing offer Waitrose marks hidden (for example "5% off 6 or more bottles of wine") does not count. `my_waitrose_offer` is true when an offer is marked for My Waitrose card holders. |
| `promotions` | One object per offer: `description` (the shelf wording), `type_code` (Waitrose's own, such as `FER`, `NLP`, `MV`), `end_date` (ISO date, null for a standing offer with no end), `price` (the pack price under the offer, null for a multi-buy), `unit_price_text`, `threshold` (units needed), `discount_type`, `discount_amount`, `discount_percent`, `hidden`, `my_waitrose`, `pick_your_own`, `promotion_id`. Empty list when none. |
| `rating`, `review_count` | Star rating out of 5 and the review count. Waitrose sends 0 for an unreviewed product; that is **null** here, with `review_count` 0. |
| `available_dates` | Seasonal lines (Christmas orders) list the windows they can be ordered for: `start_date`, `end_date`, `order_by`. Empty for a line sold all year. |

#### Honest nulls

A field Waitrose did not send is `null`, never `0` or `""`.

- **No availability field.** Waitrose's search results carry no stock flag
  (stock depends on branch and delivery slot), so the dataset has none rather
  than a guess.
- **No was-unit-price.** The only other per-unit figure Waitrose sends repeats
  the current one, so it cannot be read as the regular unit price. Compute it
  from `regular_price` if you need it.

### 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.00042** per product row written (`product-scraped`), i.e. $0.42 per
  1,000 products.

### How it fetches

It calls the JSON search API that waitrose.com's own search page uses
(`POST www.waitrose.com/api/content-prod/v2/cms/publish/productcontent/search/-1`)
with the anonymous token the site itself sends to logged-out visitors, 96
products per request, paging with `start`. Offers arrive inline, so there is one
request per page. The search page itself is also read: it is server-rendered
with the first 48 products in the page, and if the API refuses every attempt for
a term's first page, the actor reads that page instead and continues on the API
from the 49th product.

Measured 2026-10-14: HTTP 200 from a plain residential connection for both the
search page and the API. From Apify datacenter exits the search page was
reachable in an earlier test; the API call is a POST, which that test could not
make, so its behaviour on datacenter addresses is not confirmed. A refusal (403,
429, 5xx) or a 200 without the expected payload is treated as the address being
blocked, and the actor rotates to a fresh one and retries; a 400 is permanent.
If your runs are refused, switch `proxyConfiguration` to residential.

Things worth knowing, all measured:

- **Paging.** Offsets step by the requested page size, and a search with 6,297
  matches (`food`) pages to its end. The actor stops at `maxItems` per term, or
  at the last page.
- **Mid-walk empty answers.** Waitrose occasionally answers one page with an
  empty envelope while matches remain; the actor treats that as a refusal and
  retries instead of ending the term early.
- **Loose matching.** A short term returns many loosely related products. Make
  the term more specific, or use `onlyOffers`, to narrow it.
- **Some terms redirect.** Terms such as `coca cola` redirect the website to a
  category page; the API still returns the matching products.
- **Not offered.** Page size is fixed at 96 (the API refuses 100 or more), and
  the category and brand filters on the site are not wired into this actor.

# Actor input Schema

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

What you would type into the Waitrose search box, one term per line (for example semi skimmed milk, hovis bread or ben & jerry's). Each term is searched and read separately. A product that two terms both find is returned once, under the first. Waitrose matches loosely: milk returns 488 products, including chocolate, baby formula and body milk.

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

Read at most this many products for each search term. Waitrose serves up to 96 per request. Up to 10,000 per term are accepted; no search measured so far matched more than about 6,300 products.

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

The order Waitrose lists the results in. 'Relevance' is its own default. Waitrose pins a few sponsored products to the top of the first page whatever the sort, so one or two rows at the head of a price or rating sort can be out of order; they are marked is_sponsored in the output.

## `onlyOffers` (type: `boolean`):

Return only products Waitrose lists under its 'All Offers' filter. Useful for an offer watch: the milk search drops from 488 products to 139. Leave off to read the full range, with was_price and promotions filled in where an offer applies.

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

Apify proxy (automatic datacenter exits) is the default. A refused request (403, 429) is retried from a fresh address, which is what the proxy is for. In testing Waitrose served its search page to Apify datacenter exits and its search API to a plain residential connection.

## Actor input object example

```json
{
  "searchTerms": [
    "milk"
  ],
  "maxItems": 100,
  "sort": "relevance",
  "onlyOffers": false,
  "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": [
        "milk"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/waitrose-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/Hkhz3kF5chQ51nP9g/builds/Z8qgUH0pbUclzucFo/openapi.json
