# Ocado Product Scraper (`solidcode/ocado-scraper`) Actor

\[💰 $1.6 / 1K] Scrape grocery products from Ocado.com: name, brand, price, unit price, pack size, availability, ratings, review count, dietary icons, images and category path. Search by keyword or paste Ocado URLs.

- **URL**: https://apify.com/solidcode/ocado-scraper.md
- **Developed by:** [SolidCode](https://apify.com/solidcode) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 23 total users, 5 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

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

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

## Ocado Product Scraper

Pull grocery product data from Ocado.com at scale — names, brands, prices, price-per-unit, pack sizes, stock status, aggregate star ratings, dietary badges, category breadcrumbs, and product images across the full UK Ocado catalogue. Search by plain keyword like "milk" or paste any Ocado search or category URL. Built for pricing analysts, grocery-tech teams, and market researchers who need clean, structured Ocado product data without hand-crafting search links or maintaining their own scraper.

### Why This Scraper?

- **Keyword search, not just URLs** — type `milk` or `organic eggs` and each term runs its own Ocado search. No hand-building `?q=` links the way URL-only tools force you to.
- **Six sort orders on demand** — Recommended, Price low→high, Price high→low, Price per unit low→high, Price per unit high→low, and Customer rating, applied before collection.
- **Normalized price-per-unit in GBP** — every product carries a unit price with its measure (per litre, per kg, per 100g), converted from Ocado's raw pence (GBX) into clean pounds so you can compare value across pack sizes directly.
- **Regular and promo prices side by side** — every product on a price cut carries both its shelf price and its discounted price (plus both unit prices), alongside the deal text ("Now £1.40, was £2.10", "Buy any 2 for £4", "Everyday Savers"), so you can measure every discount without a second lookup.
- **Aggregate rating plus review count** — average star rating and total number of customer reviews on every product row, ready for ranking and quality analysis.
- **Dietary and attribute badges** — the actual on-pack labels (e.g. Vegetarian, Vegan, Organic, Lactose Free, Freezable) captured as a list per product.
- **Category breadcrumb trail** — the full Ocado category path for every product, so you can group and segment the catalogue by aisle without guessing.
- **Freshness signal built in** — guaranteed minimum product life (e.g. "3 days", "1 week") for perishables, plus alcohol, newly-listed, and time-restricted flags.
- **Primary image plus gallery** — a main product image URL and a de-duplicated list of additional images for every item.
- **Cost control per input** — cap results per keyword or URL, or set unlimited, and keep a run going past a single failed input.

### Use Cases

**Pricing & Competitive Intelligence**

- Track Ocado prices on staple products over time
- Compare price-per-unit across pack sizes and brands
- Monitor own-label vs. branded price gaps
- Benchmark a competitor's shelf against Ocado's catalogue

**Market & Category Research**

- Map an entire category by breadcrumb path
- Measure brand presence and assortment depth in an aisle
- Spot newly listed products with the new-item flag
- Analyse dietary-badge coverage (vegan, organic, gluten-free) across a category

**Product & Assortment Analytics**

- Build a structured product feed with images and pack sizes
- Rank products by average rating and review count
- Segment perishables by guaranteed product life
- Flag alcohol and time-restricted lines for compliance filtering

**Data Enrichment & Integration**

- Enrich an existing SKU catalogue with current Ocado pricing and stock
- Feed product data into dashboards and BI tools
- Power price-comparison and shopping-assistant apps
- Keep a nightly product feed in sync via scheduled runs

### Getting Started

#### Search by Keyword

The simplest way to start — one keyword:

```json
{
    "searchQueries": ["milk"],
    "maxResults": 100
}
```

#### Multiple Keywords, Sorted by Value

```json
{
    "searchQueries": ["organic eggs", "greek yogurt", "olive oil"],
    "sortBy": "pricePerAscending",
    "maxResults": 200
}
```

#### Paste Ocado URLs

Paste any Ocado search or category page URL — the scraper classifies each automatically:

```json
{
    "startUrls": [
        "https://www.ocado.com/search?q=coffee",
        "https://www.ocado.com/categories/fresh-chilled-food/dairy-eggs/9571f5bf-9a4c-48ff-be4b-9bdcfd080616"
    ],
    "maxResults": 500,
    "ignoreUrlFailures": true
}
```

#### Full-Featured Example

```json
{
    "searchQueries": ["cheddar cheese"],
    "startUrls": ["https://www.ocado.com/search?q=butter"],
    "sortBy": "customerRating",
    "maxResults": 0,
    "ignoreUrlFailures": true
}
```

### Input Reference

#### What to Scrape

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `searchQueries` | string\[] | `["milk"]` | Keywords to search on Ocado, such as "milk" or "organic eggs". Each keyword runs its own search. Leave empty if you are pasting URLs. |
| `startUrls` | string\[] | `[]` | Paste Ocado search or category page URLs. A search URL's keyword and sort order are replayed; category URLs must be a leaf-category page. Leave empty if you are using keywords. |

#### Options

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `sortBy` | select | `Recommended (Ocado default)` | Order products before they are collected: Recommended, Price: low to high, Price: high to low, Price per unit: low to high, Price per unit: high to low, or Customer rating: highest first. A sort inside a pasted URL takes priority. |
| `maxResults` | integer | `100` | Maximum number of products to return per search keyword or URL. Set to 0 for unlimited. |
| `ignoreUrlFailures` | boolean | `true` | Keep the run going if one keyword or URL fails, instead of stopping the whole run. |

### Output

Every product is one flat row. Here is a representative result:

```json
{
    "productId": "12345678",
    "retailerProductId": "560291011",
    "name": "Ocado British Semi Skimmed Milk",
    "brand": "Ocado",
    "promotions": ["Now £1.40, was £2.10"],
    "packSizeDescription": "2.27L (4 pints)",
    "price": 2.10,
    "promoPrice": 1.40,
    "currency": "GBP",
    "unitPrice": 0.92,
    "promoUnitPrice": 0.62,
    "unitPriceMeasure": "PER_LITRE",
    "available": true,
    "timeRestricted": false,
    "isAlcohol": false,
    "isNew": false,
    "mainImage": "https://www.ocado.com/productImages/560/56029101_0_640x640.jpg",
    "images": [
        "https://www.ocado.com/productImages/560/56029101_1_640x640.jpg"
    ],
    "attributeIcons": ["Vegetarian"],
    "averageRating": 4.7,
    "reviewCount": 328,
    "taxCodes": ["Zero rated"],
    "guaranteedProductLife": "5 days",
    "categoryPath": ["Fresh & Chilled", "Milk", "Fresh Milk"],
    "url": "https://www.ocado.com/products/ocado-british-semi-skimmed-milk/560291011",
    "searchQuery": "milk",
    "sourceUrl": null,
    "scrapedAt": "2026-07-03T14:30:00+00:00"
}
```

#### Core Fields

| Field | Type | Description |
|-------|------|-------------|
| `productId` | string | Ocado product identifier |
| `retailerProductId` | string | Retailer/SKU product id used to build the product URL |
| `name` | string | Full product name |
| `brand` | string | Brand name |
| `promotions` | string\[] | Current deal/offer descriptions (e.g. "Now £1.40, was £2.10", "Buy any 2 for £4") — empty when the product has no offer |
| `packSizeDescription` | string | Pack size text (e.g. "1 litre", "6 x 400g") |
| `url` | string | Canonical product page URL |

#### Pricing

| Field | Type | Description |
|-------|------|-------------|
| `price` | number | Regular shelf price, before any offer |
| `promoPrice` | number | Discounted price you pay right now while the product is on a price-cut offer ("Now £4, was £5", "Save 25%", "Intro Offer") — empty when there is no price cut. Multi-buy deals like "Buy any 2 for £4" stay in `promotions` |
| `currency` | string | Currency code (GBP) |
| `unitPrice` | number | Regular price per unit, normalized to GBP |
| `promoUnitPrice` | number | Discounted price per unit while on offer, normalized to GBP — empty when there is no price cut |
| `unitPriceMeasure` | string | Unit for the unit price (e.g. PER_LITRE, PER_KG) |
| `available` | boolean | Whether the product is currently in stock |
| `taxCodes` | string\[] | Tax code display names |

#### Ratings & Attributes

| Field | Type | Description |
|-------|------|-------------|
| `averageRating` | number | Aggregate customer star rating |
| `reviewCount` | integer | Total number of customer reviews |
| `attributeIcons` | string\[] | Dietary / attribute badge labels (e.g. Vegetarian, Vegan, Organic) |
| `guaranteedProductLife` | string | Guaranteed minimum product life (e.g. "5 days") |
| `categoryPath` | string\[] | Category breadcrumb trail |

#### Images & Flags

| Field | Type | Description |
|-------|------|-------------|
| `mainImage` | string | Primary product image URL |
| `images` | string\[] | Additional product image URLs |
| `isAlcohol` | boolean | Alcoholic product flag |
| `isNew` | boolean | Newly listed product flag |
| `timeRestricted` | boolean | Product has a time-of-day purchase restriction |

#### Run Context

| Field | Type | Description |
|-------|------|-------------|
| `searchQuery` | string | Originating keyword (when from a search) |
| `sourceUrl` | string | Originating listing URL (when from a URL) |
| `scrapedAt` | string | ISO timestamp of extraction |

### Tips for Best Results

- **Start small** — set `maxResults` to 10–50 on your first run to confirm the data matches your needs, then scale up.
- **Only leaf category pages list products** — when pasting a category URL, use the deepest sub-category (e.g. "Fresh Milk", not "Dairy"). Top-level category pages return no products.
- **Use `?q=` search URLs** — a pasted Ocado search URL replays its keyword and sort order. Ocado ignores non-standard keyword parameters, so stick to the standard search link.
- **Compare value with price-per-unit sort** — sort by "Price per unit: low to high" to surface the best-value pack size across brands in one pass, since pack sizes vary widely.
- **A URL's own sort wins** — if a pasted URL already contains a sort, it overrides the `sortBy` input for that URL.
- **Keep failures non-fatal** — leave `ignoreUrlFailures` on when running many keywords or URLs so one bad input never aborts the whole run.
- **Set `maxResults` to 0 for a full sweep** — use unlimited only when you genuinely want the entire result set for a keyword or category.

### Pricing

**From $1.60 per 1,000 results** — cleaner output and keyword search at a lower rate than URL-only alternatives. Bronze, Silver, and Gold subscribers pay progressively less; the table below shows total cost at each discount tier.

| Results | No discount | Bronze | Silver | Gold |
|---------|-------------|--------|--------|------|
| 100 | $0.19 | $0.18 | $0.17 | $0.16 |
| 1,000 | $1.90 | $1.80 | $1.70 | $1.60 |
| 10,000 | $19.00 | $18.00 | $17.00 | $16.00 |
| 100,000 | $190.00 | $180.00 | $170.00 | $160.00 |

A "result" is any product row in the output dataset. No compute or time-based charges — you pay per result, plus a small fixed per-run start fee.

### Integrations

Export data in JSON, CSV, Excel, XML, or RSS. Connect to 1,500+ apps via:

- **Zapier** / **Make** / **n8n** — Workflow automation
- **Google Sheets** — Direct spreadsheet export
- **Slack** / **Email** — Notifications on new results
- **Webhooks** — Trigger custom APIs on run completion
- **Apify API** — Full programmatic access

### Legal & Ethical Use

This actor is designed for legitimate price monitoring, market research, and product analysis. Users are responsible for complying with applicable laws and Ocado's Terms of Service. Collect only publicly available product information, respect intellectual property, and do not use extracted data for any unlawful purpose.

# Actor input Schema

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

Keywords to search on Ocado.com, such as 'milk' or 'organic eggs'. Each keyword runs its own search. Leave empty if you are pasting URLs below.

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

Paste Ocado.com search or category page URLs. A search URL's keyword and sort order are replayed; category URLs must point to a leaf-category page. Leave empty if you are using search keywords above.

## `ignoreUrlFailures` (type: `boolean`):

Keep the run going if one keyword or URL fails, instead of stopping the whole run. Leave on unless you want the run to stop at the first problem.

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

Order the products before they are collected. Leave as recommended to match Ocado's default ordering. A sort inside a pasted URL takes priority.

## `maxResults` (type: `integer`):

Maximum number of products to return per search keyword or URL. Set to 0 for unlimited (capped at around 100,000 products per input).

## Actor input object example

```json
{
  "searchQueries": [
    "milk"
  ],
  "startUrls": [],
  "ignoreUrlFailures": true,
  "maxResults": 100
}
```

# Actor output Schema

## `overview` (type: `string`):

Table of scraped products with key fields.

# 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"
    ],
    "startUrls": [],
    "ignoreUrlFailures": true,
    "maxResults": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("solidcode/ocado-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"],
    "startUrls": [],
    "ignoreUrlFailures": True,
    "maxResults": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("solidcode/ocado-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"
  ],
  "startUrls": [],
  "ignoreUrlFailures": true,
  "maxResults": 100
}' |
apify call solidcode/ocado-scraper --silent --output-dataset

```

## MCP server setup

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