# Sainsbury's Scraper: UK Grocery Prices (`reventadata/sainsburys-scraper`) Actor

Search Sainsbury's online groceries by any term. Get rows with shelf price, unit price, Nectar price, promotions with dates, ratings, categories and EAN barcodes. Filter by price, rating and promotions. Unofficial.

- **URL**: https://apify.com/reventadata/sainsburys-scraper.md
- **Developed by:** [reventadata](https://apify.com/reventadata) (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.00 / 1,000 results

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

## Sainsbury's Scraper: Grocery Prices, Nectar and Promotions

> **Unofficial.** This Actor is not affiliated with, authorised by, endorsed by, or in any way
> officially connected to Sainsbury's. "Sainsbury's" is a trademark of its respective owner and is
> used here only to describe what this tool reads. It reads the same public search pages a browser does.

Search the UK Sainsbury's online grocery store (sainsburys.co.uk) for any grocery term and get one clean row
per product: shelf price in pounds sterling, price per litre, kilo or item, Nectar card price, promotions with
their dates, rating, categories and barcodes.

### What does it do?

For each search term it walks the result pages of the Sainsbury's online grocery search, reads the
product data the page embeds, and saves one row per product. Only products the page itself tags as
search results are saved; tiles without that tag are not. On the `milk` page 1 capture of 2026-10-21,
11 of 66 product objects had no tag, and 9 of those 11 are the ids listed in the page's sponsored-ad
metadata; none of the 55 tagged ones is. The other 2 were not attributed.

### Why use it?

- **Prices as shoppers see them.** `price` is the price printed on the product tile. When the tile shows a
  Nectar card price, `price` is that price, `priceIsNectar` is true and `regularPrice` holds the price without it.
- **Unit prices.** `unitPrice` and `unitMeasure` (per litre, per kg or per `ea`) make products of different sizes comparable.
- **Promotions as data.** Each promotion carries its type, its text (for example `Meal Deal for £5.50`),
  whether it needs a Nectar card, its price before the promotion, and its start and end dates.
- **Filters that run before you pay.** Price band, minimum rating, promotions only, exact matches only,
  words the name or category must or must not contain.
- **Barcodes.** `eans` lists the product's EAN codes for matching against other retailers.

### How to use it

1. Put one or more grocery terms in `searchQueries`, for example `milk` or `oat milk`.
2. Set `maxItems` (rows to save) and, if you want, a sort order and filters.
3. Run the Actor and download the dataset as JSON, CSV or Excel.

#### Input

| Field | Type | Description |
|---|---|---|
| `searchQueries` | array | Grocery search terms, one search per term. |
| `maxItems` | integer | Total products to save across all queries, counted after the filters. Default 100. Queries run in the order given and the run stops once the cap is reached, so later queries may never be searched. |
| `maxPages` | integer | Result pages per query, 1 to 20. Default 6, which gives up to about 360 products per query (60 per page). The site serves pages until its own result total is used up; the Actor stops at the first empty page. |
| `sort` | string | `RELEVANCE`, `PRICE_ASC`, `PRICE_DESC` or `RATING`. The site sorts by the price on the tile in pounds, not by unit price. |
| `minPrice`, `maxPrice` | number | Price band in pounds. |
| `minRating` | number | Minimum average rating, 0 to 5. Unrated products are dropped when set. |
| `onlyPromotions` | boolean | Keep only products with at least one promotion. |
| `exactMatchesOnly` | boolean | Drop the lower-ranked related results the site appends (rows whose `relevance` is 2). |
| `includeKeywords`, `excludeKeywords` | array | Words the product name or category must / must not contain, ignoring case and accents. |
| `proxyConfiguration` | object | Apify Unblocker (default, and the only transport measured to work with this Actor's HTTP client, httpx). Plain and datacenter requests get an `Access Denied` page; a residential proxy returned it on 25 of 25 test pages with this Actor's HTTP client. |

#### Sample input

```json
{
    "searchQueries": ["milk"],
    "maxItems": 20,
    "maxPages": 1,
    "sort": "PRICE_ASC"
}
```

#### Output

One row per product:

| Field | Type | Description |
|---|---|---|
| `id` | string | Sainsbury's product id. |
| `name` | string | Product name. |
| `url` | string | Product page URL. |
| `price` | number | Shelf price in GBP, the Nectar price when `priceIsNectar` is true. |
| `currency` | string | Always `GBP`. |
| `unitPrice`, `unitMeasure` | number / string | Price per unit and the unit, or null. Values seen: `ltr`, `kg` and `ea`. |
| `regularPrice` | number / null | The price before the product's fixed-price promotion (`SIMPLE_FIXED_PRICE`), or null when it has none. Multibuy and meal-deal promotions do not set it; their `price` is the normal shelf price. |
| `priceIsNectar` | boolean | True when `price` needs a Nectar card. |
| `onPromotion` | boolean | True when the product has at least one promotion. |
| `promotions` | array | `type`, `text`, `isNectar`, `regularPrice`, `startsAt`, `endsAt` for each promotion. Inside a promotion, `regularPrice` is the product's price before the promotion: for a fixed-price promotion it is higher than `price`, for multibuy and meal deals it equals `price`. |
| `rating` | number / null | Average rating out of 5, null when the product has no reviews. |
| `reviewCount` | integer | Number of reviews, `0` when there are none. |
| `categories` | array | Category names. |
| `image` | string | Image URL. |
| `eans` | array | EAN barcodes. |
| `isAvailable` | boolean | Availability flag the site reports. |
| `relevance` | integer | 1 for an exact match, 2 for a related result. |
| `matchedKeyword` | string | The query that returned the product. |
| `page` | integer | Result page the product came from. |
| `scrapedAt` | string | ISO 8601 timestamp. |

#### Sample output

Taken from a real row (2026-10-21), with `scrapedAt` set to a fixed time:

```json
{
    "id": "7657802",
    "name": "Sainsbury's 2L Fresher for Longer Filtered Semi-Skimmed Milk",
    "url": "https://www.sainsburys.co.uk/groceries/product/sainsburys-2l-fresher-for-longer-filtered-semi-skimmed-milk",
    "price": 1.95,
    "currency": "GBP",
    "unitPrice": 0.98,
    "unitMeasure": "ltr",
    "regularPrice": 2.2,
    "priceIsNectar": true,
    "onPromotion": true,
    "promotions": [
        {
            "type": "SIMPLE_FIXED_PRICE",
            "text": "Buy 1 for 1.95",
            "isNectar": true,
            "regularPrice": 2.2,
            "startsAt": "2026-10-06T23:00:00Z",
            "endsAt": "2026-10-27T00:00:00Z"
        }
    ],
    "rating": 4.77,
    "reviewCount": 362,
    "categories": [
        "Fresh milk",
        "Dairy & chilled essentials"
    ],
    "image": "https://assets.sainsburys-groceries.co.uk/gol/7657802/image.jpg",
    "eans": [
        "0000000133845"
    ],
    "isAvailable": true,
    "relevance": 1,
    "matchedKeyword": "milk",
    "page": 1,
    "scrapedAt": "2026-10-21T12:00:00+00:00"
}
```

### Limits

- **Results per query follow the site's own total.** Measured 2026-10-21 with `maxPages` 7 or more: `milk` had 329 results over 6
  pages, `bread` 371 over 7 pages (page 7 held 11 products) and `chocolate` 770 over 13 pages. With the default
  `maxPages` of 6 a query stops at about 360 products, so raise `maxPages` (ceiling 20, about 1,200 products) for broad terms.
  Beyond 770 results for one term was not measured; split a very broad term into several narrower searches.
- **Some results are related products, not exact matches.** On `bread` 228 of 368 rows had `relevance` 2. Turn on
  `exactMatchesOnly` to keep only `relevance` 1.
- Prices and promotions are what the site serves to an anonymous visitor with no postcode, store or delivery slot set; the proxy exit's location is not pinned.
- Prices are in pounds sterling (`GBP`).
- Rows are read from the page as served; the Actor reports a failed run when it cannot read any product.
- A run that saves zero rows fails instead of finishing empty, so a mistyped term or an over-strict filter is visible.

### How fast is it?

About 5 to 7 seconds per result page of up to 60 products. Measured 2026-10-21: 769 `chocolate` products (site total 770)
over 14 pages (the last one empty) in 94 seconds. `bread` (7 pages, 368 products) took 33 seconds.

### How much does it cost to scrape Sainsbury's?

Pay-per-event: **$0.001 per result** saved to the dataset (**$1.00 per 1,000 results**), plus **$0.00005** charged once
when the run starts. No subscription. Filters run before a row is saved, so rows they drop are not charged.

### FAQ

**Is a proxy required?** Yes. Sainsbury's returns an `Access Denied` page to plain and datacenter requests. The Actor
defaults to Apify Unblocker, the transport that returned a real search page on 25 of 25 test searches (2026-10-21).

**Is `price` the Nectar price?** When the tile shows a Nectar card price, yes: `priceIsNectar` is true and `regularPrice`
holds the price without the card. Otherwise `price` is the plain shelf price.

**Why is `rating` null but `reviewCount` 0?** The product has no reviews. In 1,137 rows from two runs on 2026-10-21, 194
had no reviews: `rating` null, `reviewCount` 0.

**Why do I get products that are not what I typed?** After the exact matches the site appends related products.
They are marked `relevance` 2; `exactMatchesOnly` drops them.

**Does it cover stores outside the UK?** No. It reads sainsburys.co.uk only, which is the UK storefront.

**Does it use my Sainsbury's or Nectar account?** No. It sends no login and sees what an anonymous visitor sees.

### More scrapers

| Actor | What it scrapes |
|---|---|
| [Amazon Product & Price Scraper](https://apify.com/reventadata/amazon-scraper) | Amazon product listings and prices |
| [Idealo Price Comparison Scraper](https://apify.com/reventadata/idealo-scraper) | Idealo price comparison listings |
| [Gumtree UK Scraper](https://apify.com/reventadata/gumtree-scraper) | Gumtree UK, any category, second-hand listings |
| [AliExpress Product & Price Scraper](https://apify.com/reventadata/aliexpress-scraper) | AliExpress products and prices |
| [Shopify Store Monitor](https://apify.com/reventadata/shopify-scraper) | Price and stock changes on Shopify stores |

# Actor input Schema

## `searchQueries` (type: `array`):

Grocery search terms, one search per term on sainsburys.co.uk (for example milk, oat milk, olive oil).

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

Total number of products to save across all queries. Each result page holds up to 60 products.

## `maxPages` (type: `integer`):

Result pages to walk per query (up to 60 products each). The default of 6 gives about 360 products; raise it for broad terms. The site serves pages until its own result total is used up and the Actor stops at the first empty page.

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

Order of the search results on the site.

## `minPrice` (type: `number`):

Keep only products priced at or above this amount in pounds. Leave empty for no minimum.

## `maxPrice` (type: `number`):

Keep only products priced at or below this amount in pounds. Leave empty for no maximum.

## `minRating` (type: `number`):

Keep only products with an average customer rating at or above this value (0 to 5). Unrated products are dropped when this is set.

## `onlyPromotions` (type: `boolean`):

Keep only products that carry at least one promotion (Nectar price, meal deal, multibuy or price drop).

## `exactMatchesOnly` (type: `boolean`):

Drop the lower-ranked related results the site appends after the exact matches (rows whose relevance is 2).

## `includeKeywords` (type: `array`):

Keep only products whose name or category contains every one of these words, ignoring case and accents.

## `excludeKeywords` (type: `array`):

Drop products whose name or category contains any of these words, ignoring case and accents.

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

Apify Unblocker is required: no proxy and datacenter proxies get an Access Denied page from Sainsbury's.

## Actor input object example

```json
{
  "searchQueries": [
    "milk"
  ],
  "maxItems": 20,
  "maxPages": 1,
  "sort": "RELEVANCE",
  "onlyPromotions": false,
  "exactMatchesOnly": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

Items scraped from the target website.

# 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 = {
    "searchQueries": [
        "milk"
    ],
    "maxItems": 20,
    "maxPages": 1,
    "sort": "RELEVANCE",
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "UNBLOCKER"
        ]
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("reventadata/sainsburys-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 = {
    "searchQueries": ["milk"],
    "maxItems": 20,
    "maxPages": 1,
    "sort": "RELEVANCE",
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["UNBLOCKER"],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("reventadata/sainsburys-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 '{
  "searchQueries": [
    "milk"
  ],
  "maxItems": 20,
  "maxPages": 1,
  "sort": "RELEVANCE",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "UNBLOCKER"
    ]
  }
}' |
apify call reventadata/sainsburys-scraper --silent --output-dataset

```

## MCP server setup

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