# Woolworths & Aldi Price Scraper and Comparison (`sz022/woolworths-aldi-price-scraper`) Actor

Woolworths and Aldi Australia prices in one table: price, was-price, unit price, specials, multibuys, stock, barcode, nutrition and Special Buy dates. Search terms, category or product links. Optional: match the same product at both stores, with a confidence score. No login.

- **URL**: https://apify.com/sz022/woolworths-aldi-price-scraper.md
- **Developed by:** [CHEN-SHU-CHEN](https://apify.com/sz022) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 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

### What is Woolworths & Aldi Price Scraper and Comparison?

You want to know what a basket costs at Woolworths and at Aldi this week, which items are on special, or how a brand's products sit on the shelf. Checking two websites by hand means copying prices, sizes and unit prices product by product, and the two stores write sizes and unit prices differently.

Type **search terms**, paste **category or product links**, or both, and get **one row per product** from **Woolworths** and **Aldi Australia** in the same columns: **price**, **was-price** and **savings**, **unit price** (also converted to per kg, per litre or per item, so the stores compare directly), **special type** (half price, multibuy, price drop, member and Rewards offers), **in stock**, **barcode**, **category**, **all images**, and for Woolworths the **ingredients, allergens, Health Star Rating and nutrition** that come with every search result. No login is needed.

Turn on **compare stores** and the Actor also pairs the **same product at both stores** (same brand, same pack size, matching name) and writes one comparison row per pair: **which store is cheaper**, by how much, and by unit price, with a **match confidence** score so you can see why two products were paired.

Because it runs on Apify, you can **schedule** a weekly price check, call it through the **API**, connect it to Make, Zapier or Google Sheets, and export as **CSV, Excel or JSON**.

**Coles is not supported.** Its site blocks automated reads, so this Actor covers Woolworths and Aldi only, and says so rather than returning half-empty Coles rows.

### How to scrape Woolworths and Aldi prices

1. Open the **Input** tab and add **Search terms**, one per line: `cadbury dairy milk`, `coca cola 2l`, `nappies`. Each term is searched at both stores unless you pick one.
2. Optionally paste **Category links** (`https://www.woolworths.com.au/shop/browse/specials/half-price`, `https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160`) or **Product links or codes** (`https://www.woolworths.com.au/shop/productdetails/44528/...`, a stockcode such as `44528`, or an 18-digit Aldi SKU).
3. Set **Max products per search term or category**. 50 is the default.
4. Turn on what you need: **Specials only**, **Add Aldi Special Buys**, **Include product page details**, **Compare the same product at both stores**.
5. Click **Start**. A search term with 50 products per store finishes in a few seconds.
6. Open the **Output** tab. The *Products* view lists every product, *Woolworths vs Aldi* the comparison rows, and *Inputs and errors* anything that returned nothing.

### What data does it return?

Every product row has the same columns for both stores. A column a store does not publish is `null`, never `0`.

| Field | Notes |
|---|---|
| `store`, `productId`, `stockcode` / `sku`, `url` | Woolworths stockcode or Aldi SKU |
| `barcode`, `gtinFormat` | Woolworths only. Aldi publishes no barcodes |
| `name`, `displayName`, `brand`, `variety`, `size` | |
| `sizeAmount`, `sizeUnit`, `packCount` | The size normalised: `6 x 375mL` is `2250`, `ml`, `6` |
| `price`, `wasPrice`, `savings`, `isOnSpecial`, `isHalfPrice` | `wasPrice` only when it is above the price |
| `specialTypes` | `half_price`, `multibuy`, `member_price`, `rewards_offer`, `prices_dropped`, `lower_price_always`, `special`; for Aldi `special_buy`, `lower_prices`, `super_savers`, `limited_time_only` |
| `promotionTexts` | The store's own tag text: "Save $18.00", "2 for $10.00", "Was $3.30 - 25/08/2026" |
| `multibuyQuantity`, `multibuyPrice`, `multibuyUnitPrice` | "2 for $10" is `2`, `10.0` |
| `wasPriceHistory` | Woolworths "Prices Dropped" tags parsed: `{price: 3.3, date: "2026-08-25", isRange: false}` |
| `unitPrice`, `unitPriceMeasure`, `unitPriceText` | As the store shows it: `2.7`, `100G`, `$2.70 / 100G` |
| `unitPriceStandard`, `unitPriceStandardUnit` | Per `kg`, `L` or `each`, the same basis for both stores |
| `instorePrice`, `instoreWasPrice`, `instoreIsOnSpecial` | Woolworths' in-store price where it differs |
| `inStock`, `isAvailable`, `isPurchasable`, `supplyLimit`, `stockMessage` | Woolworths. Aldi publishes no stock, so `inStock` is `null` there |
| `isForDelivery`, `isForCollection`, `isForExpress`, `nextAvailabilityDate` | Woolworths delivery options |
| `isSponsored`, `adStatus`, `searchRank`, `organicRank`, `inputTotalResults` | Position in the store's results for your term, with and without sponsored tiles: share of shelf for a brand |
| `isMarketplace`, `vendor` | Woolworths Everyday Market (third-party sellers) |
| `categoryPath`, `departments`, `categories`, `subcategories`, `categoryIds`, `sap*` | |
| `ingredients`, `allergensContains`, `allergensMayContain`, `allergyStatement`, `dietaryClaims`, `healthStarRating`, `nutrition`, `description`, `storageInstructions` | Woolworths, from the search result itself |
| `imageUrl`, `images`, `imageUrlMedium`, `imageUrlSmall`, `imageDetails` | All product images; Aldi at 1500 px |
| `isSpecialBuy`, `specialBuyOnSaleDate`, `specialBuyOnSaleText`, `isUpcomingSpecialBuy`, `specialBuyWeek` | Aldi Special Buys |
| `averageRating`, `ratingCount`, `reviewCount`, `ratingBreakdown` | `null` when the store shows no ratings |
| `fulfilmentStoreId` | The Woolworths store the price comes from (see below) |
| `input`, `inputType`, `inputIndex`, `status`, `scrapedAt` | Which input found the product |

**With Include product page details** (one extra request per product):

| Field | Store |
|---|---|
| `nutritionPanel` (per serve and per 100 g/mL), `servingSize`, `servingsPerPack` | Woolworths |
| `countryOfOrigin`, `australianIngredientsPercent`, `countryOfOriginLabel` | Woolworths: "Australian", `100` |
| `aisle`, `aisleDepartment`, `detailImages`, `videoUrl`, `directions`, `warnings` | Woolworths |
| `description`, `ingredients`, `allergensContains`, `allergensMayContain`, `storageInstructions`, `directions` | Aldi. Aldi fills ingredients and storage on about a third of its products |

`detailsStatus` says whether that worked for each row.

**Comparison rows** (`rowType: "comparison"`): `cheapestStore` (`woolworths`, `aldi` or `same`), `priceDifference`, `priceDifferencePercent`, `cheapestByUnitPrice`, `unitPriceDifference`, `unitPriceUnit`, `matchMethod`, `matchConfidence`, `nameSimilarity`, and each side's id, name, brand, size, price, was-price, unit price, link and image.

**Input rows** (`rowType: "input"`): one for each input that produced no product rows, with `status` (`no_results`, `not_found`, `invalid_input`, `blocked`, `parse_error`, `partial`, `not_processed`) and `error`. The outcome of every input, including the successful ones, is also in the key-value store under `INPUTS`.

**Unreadable products**: if a store sends a product without an id, name, link or price, or in a shape the parser cannot read, it becomes a free row with `status: "parse_error"` and the reason in `error`, and the rest of the input carries on.

### How does the Woolworths vs Aldi comparison work?

Aldi publishes no barcodes, so products are matched on what both stores do publish:

1. **Brand must be the same** after tidying case and punctuation: `Cadbury` = `CADBURY`.
2. **Pack size must be the same** total amount within 2%: `2L` = `2 l`, `1kg` = `1000 g`. A six-pack never matches a single bottle, even at the same volume.
3. **The rest of the name** is compared once brand and size are removed, by shared words and character similarity.

Brand and size are gates, not points. A pair that fails either is never matched. Passing both gives 0.6, and the name adds up to 0.4, so `matchConfidence` runs from 0.6 to 1. Only pairs at or above **Minimum match confidence** (0.85 by default) are written, and each product is used at most once, best match first.

In practice this pairs the branded products both stores stock (Cadbury, Coca-Cola, Norco), which is where a like-for-like price comparison is honest. Aldi's own brands have no Woolworths twin, so they are not paired with a Woolworths product that is only similar. If you want looser pairs, lower the threshold and check `nameSimilarity`.

Only products read in the same run are compared, so search the same terms at both stores. A product found by two terms is written once.

### How much does it cost to scrape Woolworths and Aldi?

You pay per row, not per minute.

| Event | Charged for | Price | Per 1,000 |
|---|---|---|---|
| `product-scraped` | One product row, from either store | $0.0015 | $1.50 |
| `product-details` | The product page added to that row (Include product page details) | $0.0005 | $0.50 |
| `comparison-row` | One product matched at both stores | $0.005 | $5.00 |

Free, never charged: input rows (`no_results`, `not_found`, `invalid_input`, `blocked`, `parse_error`, `not_processed`), unreadable products (`parse_error`), a product found again by a second term, and product page details when the input was itself a product link (that page is the request).

Examples: 50 products from each store for 10 terms is 1,000 rows, $1.50. With details, $2.00. Comparison rows only appear for confident matches, typically a handful per branded term.

Apify's free plan includes $5 of usage a month, about 3,300 products.

#### How do I keep a run from costing more than I expect?

Set a maximum charge for the run when you start it. Before each product, the Actor checks that the budget still covers it and its details together (if details are on), and stops before fetching anything it could not charge; inputs it did not reach are listed as `not_processed`. Each row is written and charged in one step by Apify's SDK, which writes only what the budget covers, so a row is never charged without being in your dataset. Details are charged only after their row is written, once per product. If Apify cannot record a charge, the run stops its paid work. A run also stops when it is plainly costing more than it earns, for example through a proxy that stalls every request, and shortly before the run's timeout: every request's timeout is cut to the time left, and what was not reached is `not_processed`.

#### What happens if Apify restarts a run?

Progress is saved after every input, every 25 products, and whenever Apify announces a migration. A restarted run reads its own dataset, skips every product and comparison row already written, finishes the input it was on without going over its product limit, and carries on, so rows are not written or charged twice. If the dataset cannot be read back, the run stops and fails rather than guess.

Two narrow windows remain. If the run stops between writing a product and charging its details, those details are never charged. And because a write can take a moment to show when the dataset is read back, the row written just before a restart can occasionally be missed, then written and charged a second time. That affects at most one row per restart.

### Input example

```json
{
  "searchTerms": ["cadbury dairy milk", "coca cola 2l"],
  "stores": "both",
  "maxProductsPerInput": 20,
  "categoryUrls": ["https://www.woolworths.com.au/shop/browse/specials/half-price"],
  "specialsOnly": false,
  "compareStores": true
}
```

### Output example

From a real run on 2026-10-18. A comparison row, trimmed:

```json
{
  "rowType": "comparison",
  "cheapestStore": "aldi",
  "priceDifference": 1.01,
  "priceDifferencePercent": 33.7,
  "cheapestByUnitPrice": "aldi",
  "unitPriceDifference": 22.5,
  "unitPriceUnit": "kg",
  "matchMethod": "brand_size_name",
  "matchConfidence": 1.0,
  "woolworthsName": "Cadbury Dairy Milk Chocolate Bar 45g",
  "woolworthsPrice": 3.0,
  "woolworthsUnitPriceStandard": 66.7,
  "aldiName": "Dairy Milk Chocolate Bar 45g",
  "aldiPrice": 1.99,
  "aldiUnitPriceStandard": 44.2
}
```

A Woolworths product row, trimmed:

```json
{
  "store": "woolworths",
  "productId": "814479",
  "name": "Cadbury Dairy Milk Chocolate Block",
  "brand": "Cadbury",
  "size": "180g",
  "price": 5.5,
  "wasPrice": 8.0,
  "savings": 2.5,
  "isOnSpecial": true,
  "specialTypes": ["special"],
  "promotionTexts": ["30% Off", "Save $2.50"],
  "unitPriceText": "$3.06 / 100G",
  "unitPriceStandard": 30.6,
  "unitPriceStandardUnit": "kg",
  "inStock": true,
  "barcode": "9300617064879",
  "isSponsored": false,
  "searchRank": 2,
  "categoryPath": ["Halloween", "Confectionery", "Chocolate Blocks"],
  "url": "https://www.woolworths.com.au/shop/productdetails/814479/cadbury-dairy-milk-chocolate-block"
}
```

### FAQ

#### Which store's prices are these?

**Woolworths:** the prices of Woolworths' default online store (`fulfilmentStoreId` 1101). Most prices are national, but some fresh and regional items differ by store. Choosing another store needs a logged-in Woolworths account, so there is no postcode setting.

**Aldi:** Aldi's national in-store (walk-in) prices. Aldi does not sell groceries online, and its prices do not change by store.

So a comparison row puts Woolworths' online price next to Aldi's shelf price.

#### What does "Specials only" mean at Aldi?

Woolworths filters its own specials. Aldi shows no was-prices at all, so "specials" at Aldi means the products in its own **Lower Prices**, **Super Savers** and **Limited Time Only** groups, and `specialTypes` says which. Aldi's weekly **Special Buys** are a separate option, because they are mostly non-food (tools, homewares, clothing) on sale from a given date while stocks last.

#### What can it not return?

- **Coles**, whose site blocks automated reads.
- **Aldi barcodes and stock levels**, which Aldi does not publish. `barcode` and `inStock` are `null` on Aldi rows.
- **Ratings** for most products: Woolworths sends zeros in search results, which are reported as `null`, not as a rating of 0.
- **Prices for a specific Woolworths store or postcode** (see above).

#### Is it legal to scrape supermarket prices?

The Actor reads the same public product data any visitor sees on woolworths.com.au and aldi.com.au, without an account. Check the rules that apply to you and to how you use the data; this is not legal advice.

#### Why did an input come back blocked?

Woolworths and Aldi both read through Apify's datacenter proxy in every test. If a store refuses a request, the Actor retries on a different IP up to three times, then marks the input `blocked`. After three blocked inputs in a row it stops, and the rest come back `not_processed` at no charge. Run again later. Residential and Google SERP proxy groups are not used, because the price per product is set for datacenter traffic.

#### Other limits worth knowing

- Results come in each store's own relevance order. Woolworths mixes sponsored products into search results; they are kept and flagged `isSponsored`, because their position is part of what the shelf looks like.
- Both stores' APIs are undocumented and can change without notice. If a store's answer stops making sense, the input is reported `parse_error` rather than filled with guesses.
- Up to 1,000 products per input, and 1,000 Special Buys.

#### Something broke or a number looks wrong

Open an issue on the **Issues** tab with the input and the run link.

# Actor input Schema

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

Words you would type into the store's search box, one per line: "cadbury dairy milk", "coca cola 2l", "nappies". Each term is searched at every store selected below.

## `stores` (type: `string`):

Which stores the search terms run against. Category and product links always go to the store they belong to. Coles is not supported.

## `categoryUrls` (type: `array`):

Category or specials pages copied from the browser, for example https://www.woolworths.com.au/shop/browse/dairy-eggs-fridge/milk, https://www.woolworths.com.au/shop/browse/specials/half-price or https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160.

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

Product pages (https://www.woolworths.com.au/shop/productdetails/44528/..., https://www.aldi.com.au/product/...-000000000000398691), a Woolworths stockcode (44528) or an 18-digit Aldi SKU. One request each, and the product page's details are included at no extra charge.

## `maxProductsPerInput` (type: `integer`):

Per store: "milk" with both stores selected returns up to this many from Woolworths and this many from Aldi, in the store's own relevance order.

## `specialsOnly` (type: `boolean`):

Woolworths: only products on special (half price, multibuy, price drops, other specials). Aldi shows no was-prices, so here it means products in Aldi's own Lower Prices, Super Savers and Limited Time Only groups.

## `includeAldiSpecialBuys` (type: `boolean`):

Also list Aldi's current and upcoming Special Buys (the weekly Wednesday and Saturday ranges), with each one's on-sale date. Charged as ordinary products.

## `maxSpecialBuys` (type: `integer`):

Upper limit on Special Buys rows when the option above is on.

## `includeDetails` (type: `boolean`):

One extra request per product. Woolworths: nutrition per serve and per 100 g with serving size, country of origin with the % of Australian ingredients, aisle, all images. Aldi: description, ingredients, allergens, storage and usage notes. Charged as product-details on top of the product. Search rows already carry Woolworths ingredients, allergens, Health Star Rating and the nutrition values without this.

## `compareStores` (type: `boolean`):

After reading, pair products sold at both Woolworths and Aldi (same brand, same pack size, similar name) and add one comparison row per pair: cheapest store, price difference, unit-price difference and the match confidence. Needs products from both stores in the run. Charged per comparison row.

## `matchThreshold` (type: `string`):

Comparison rows are only written for pairs at or above this confidence (0.6 to 1). Brand and pack size must always agree; the score then reflects how similar the rest of the name is. 0.85 is strict enough that pairs read as the same product; lower it to see looser pairs.

## `abortAfterConsecutiveFailures` (type: `integer`):

Ends the run early when a store stops answering. Inputs with no results or that do not exist do not count. 0 never stops early.

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

Apify's datacenter proxy, which read both stores in every test. Residential and Google SERP groups are not used: the price is set for datacenter traffic, so a run that asks for them falls back to datacenter. Proxy URLs of your own are used as given.

## Actor input object example

```json
{
  "searchTerms": [
    "cadbury dairy milk"
  ],
  "stores": "both",
  "maxProductsPerInput": 10,
  "specialsOnly": false,
  "includeAldiSpecialBuys": false,
  "maxSpecialBuys": 200,
  "includeDetails": false,
  "compareStores": true,
  "matchThreshold": "0.85",
  "abortAfterConsecutiveFailures": 3,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `products` (type: `string`):

Product rows (rowType product), comparison rows (rowType comparison) and input status rows (rowType input), in the order they were read.

## `inputs` (type: `string`):

Each search term, link and code with its status, products written and duplicates skipped.

# 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": [
        "cadbury dairy milk"
    ],
    "maxProductsPerInput": 10,
    "compareStores": true,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("sz022/woolworths-aldi-price-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 = {
    "searchTerms": ["cadbury dairy milk"],
    "maxProductsPerInput": 10,
    "compareStores": True,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("sz022/woolworths-aldi-price-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 '{
  "searchTerms": [
    "cadbury dairy milk"
  ],
  "maxProductsPerInput": 10,
  "compareStores": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call sz022/woolworths-aldi-price-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sz022/woolworths-aldi-price-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/OCzjYG4SiivzlKdTT/builds/vhpSGZ592UNQhHsPf/openapi.json
