# Woolworths Australia Grocery Prices Scraper (`dj_orona/au-grocery-prices`) Actor

Woolworths Australia product prices, was-prices, unit prices, specials, stock and GTIN from the public website. Woolworths only.

- **URL**: https://apify.com/dj_orona/au-grocery-prices.md
- **Developed by:** [dj orona](https://apify.com/dj_orona) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.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

## Woolworths Australia Grocery Prices Scraper

Returns grocery product records from the public Woolworths Australia website. Each record has the name, brand, size, price, was-price, unit price, special and in-stock flags, the barcode (GTIN) where Woolworths shows it, the product URL and the image URL. It's for developers and AI agents doing Australian grocery price tracking, basket costing and promotion monitoring.

Independent tool, not affiliated with or endorsed by Woolworths Group.

**Limits:**

- **Woolworths only.** Coles isn't supported. If that changes, this page will say so.
- Prices are what Woolworths' **default online store/region shows a logged-out visitor** at scrape time. They may differ from your local store or from what you see when logged in. You can't choose a store.
- If Woolworths blocks the request or its robots.txt disallows a path we use, the run stops with a clear status instead of trying to get around it (see "How this Actor behaves").
- Each run is a snapshot. There's no price history.

### What you can do with it

- Track the prices of a list of products over time by scheduling runs.
- Cost a shopping basket at Woolworths online prices.
- See which products are on special right now, and by how much (`price` vs `wasPrice`).

### Input

Give at least one of `searchTerms` or `productUrls`.

| Field | Type | Default | What it does |
|---|---|---|---|
| `searchTerms` | string\[] | none (prefill `["milk 2L"]`) | Keywords such as `milk 2L` or `weet-bix`, up to 100 characters each. Each term returns matching products (24 per page, paged automatically) until `maxItems` is reached. |
| `productUrls` | string\[] | none | Woolworths product URLs (`https://www.woolworths.com.au/shop/productdetails/<id>/<slug>`) or bare numeric product IDs such as `888137`. Query strings are ignored. These run before search terms. |
| `maxItems` | integer | 100 (min 1, max 5000) | Maximum number of products returned across all inputs. This also caps your cost. |
| `chains` | string\[] | `["woolworths"]` | Only `["woolworths"]` is accepted. Anything else gives `INVALID_INPUT`. |
| `includeRunReport` | boolean | `false` | For testing. Puts one extra `recordType: "run-report"` row first in the dataset. **That row counts as one charged result**, and it's written even when the run ends `FAILED`, `BLOCKED` or `ROBOTS_BLOCKED`. The same report is always saved free as `RUN_SUMMARY` in the key-value store. Status rows are not charged. |

A product already returned by an earlier input isn't repeated in the same run. Blank entries are ignored. There's no proxy setting and no concurrency setting: the Actor runs from Apify's servers without a proxy and makes one request at a time.

Example input:

```json
{ "searchTerms": ["milk 2L", "weet-bix"], "productUrls": ["888137"], "maxItems": 60 }
```

### Output

One dataset item per product, with exactly these 15 fields. Prices are in **AUD**. Timestamps are ISO 8601 UTC. A field the site doesn't provide is `null`, never missing. `chain`, `productId`, `name`, `onSpecial`, `url` and `scrapedAt` are always filled.

| Field | Type | Meaning |
|---|---|---|
| `chain` | string | Always `woolworths` in this version |
| `productId` | string | Woolworths' product ID (Stockcode) |
| `gtin` | string | null | Barcode (GTIN/EAN, 8–14 digits) when Woolworths provides it, otherwise null. In one sample run on 2026-10-03, 50 of 59 search results had a GTIN. Coverage varies by product. |
| `name` | string | Product name as shown |
| `brand` | string | null | Brand |
| `size` | string | null | Pack size as shown, e.g. `2L`, `500g` |
| `price` | number | null | Current price in AUD. Null if Woolworths shows no price (or a zero price). |
| `wasPrice` | number | null | Previous price in AUD. **Set only when the item is on special** (and higher than `price`), so it's null for most items. |
| `unitPrice` | number | null | Price per `unitMeasure` in AUD |
| `unitMeasure` | string | null | Unit for `unitPrice`, e.g. `1L`, `100g`, `1EA` |
| `onSpecial` | boolean | True if Woolworths marks the product as on special |
| `inStock` | boolean | null | Online availability flag as shown, or null if unknown |
| `url` | string | Product page URL (the source), no query string |
| `image` | string | null | Woolworths' product image URL (linked, not copied or re-hosted) |
| `scrapedAt` | string | ISO 8601 UTC time the item was collected |

Real example item from a test run on 2026-10-03 (`image` and `scrapedAt` left out of this excerpt):

```json
{
  "chain": "woolworths",
  "productId": "44528",
  "gtin": "9322042000048",
  "name": "Norco Full Cream Milk",
  "brand": "Norco",
  "size": "2L",
  "price": 4.5,
  "wasPrice": 5.05,
  "unitPrice": 2.25,
  "unitMeasure": "1L",
  "onSpecial": true,
  "inStock": true,
  "url": "https://www.woolworths.com.au/shop/productdetails/44528/norco-full-cream-milk"
}
```

A full run report (each input's status, item counts, null counts and errors) is always saved as `RUN_SUMMARY` in the run's key-value store.

### Run statuses

The Actor's status message starts with one of these. `RUN_SUMMARY.status` in the key-value store holds the same value.

| Status | Exit | Meaning |
|---|---|---|
| `SUCCEEDED` | ok | Every input was processed. Some search terms may have had no results (`empty`). |
| `PARTIAL` | ok | Some products came back, but at least one input was invalid, not found or errored. See `RUN_SUMMARY.cases`. |
| `FAILED` | fail | No products, and at least one input failed or an unexpected error happened. |
| `INVALID_INPUT` | fail | Something's wrong with the input. The message names the field. No requests are made. |
| `ROBOTS_BLOCKED` | fail | robots.txt didn't return 200, couldn't be parsed, or now disallows an endpoint we use. No product requests are made. |
| `BLOCKED` | fail | Woolworths returned 403/401/429 or a challenge page 3 times (2 fresh-session retries at least 30 s apart). Products already saved stay in the dataset. |

Per-input statuses in `RUN_SUMMARY.cases`: `ok, empty, partial, error, not_found, invalid_input, skipped_robots, skipped_max_items, blocked, not_run`. These statuses are recorded only in `RUN_SUMMARY` (and in the optional run-report row). They are never written as separate dataset items. An unknown product ID returns `not_found` with a message. A non-Woolworths URL returns `invalid_input` with a message. Neither one produces a dataset item. If product lookups fail only because of network errors, the message suggests using `searchTerms` instead.

### Pricing

- **$2.00 per 1,000 products returned** ($0.002 each). No start fee.
- A result is one product item in the dataset. **Status rows are not charged:** failed, blocked, not-found and invalid inputs produce no dataset item, so they cost nothing.
- The one exception: with `includeRunReport: true`, the report row is a dataset item and counts as one result ($0.002). It's written even when the run ends `FAILED`, `BLOCKED` or `ROBOTS_BLOCKED`, so such a run costs $0.002 plus any products already saved. It isn't written for `INVALID_INPUT`.
- Examples: a default run (up to 100 products) costs at most $0.20. A 20-product run costs $0.04. `maxItems` caps the cost of any run.

### Reliability

Release test: build woolworths-v1.3, 2026-10-03, 6 test cases × 20 runs each, every case passed at least 19 of 20 runs. This is a short test window, not a long-term uptime figure.

After release we run a daily check. Please report problems on the Issues tab.

### How this Actor behaves (compliance and politeness)

1. **Public data only.** Logged-out GET requests to Woolworths' public search API, the public product page and, as a last resort, the public product JSON endpoint. No login, account, cart, checkout or store/region selection.
2. **Challenges and blocks stop the run.** On a 403 "Access Denied", 401, 429, CAPTCHA or bot-check page, the request is never solved, clicked through or bypassed. The Actor retries at most twice, each time on a fresh session after at least 30 s (60 s for the second retry). It honours Retry-After up to 300 s. After that the run ends with `BLOCKED`. This applies to every endpoint.
3. **No cookies, no tokens.** The Actor stores, sends and imports no cookies. No CAPTCHA solvers, unblocking APIs or stealth plugins.
4. **Headers.** Standard generic browser-style headers, kept the same for the whole session. A new set is generated only for the fresh-session retries above. No Referer or cookie spoofing.
5. **No proxy.** Runs directly from Apify's servers. No proxy rotation or fallback after a block.
6. **Rate limits.** One request at a time, at least 2.5 s apart, capped by `maxItems`. Server or network errors get at most two short retries before the next method is tried.
7. **robots.txt.** Read first on every run, and the run stops if it can't be read. Every request path is checked against it right before sending. Redirects are never followed automatically. The site-search page `/shop/search` is never used.
8. **Product data only.** Only the 15 fields above. No ratings, reviews, reviewer or customer data.

### FAQ

**Is Coles supported?** No, only Woolworths.

**Which store's prices do I get?** Woolworths' default online store/region for a logged-out visitor. You can't pick a store in this version.

**Why is `wasPrice` usually null?** Woolworths only shows a previous price when an item is on special. Check `onSpecial`.

**Why is `gtin` sometimes null?** Woolworths doesn't provide a barcode for every product. In one sample run on 2026-10-03, 9 of 59 search results had none. Coverage varies by product.

**Can I match products with other retailers?** The Actor doesn't do that. `gtin` is included when available, so you can use it for your own matching.

**My run ended with BLOCKED or ROBOTS_BLOCKED. Why?** Woolworths refused the requests, or its robots.txt no longer allows a path we use. We stop instead of working around it. Products already returned stay in the dataset and are charged as normal. Nothing else is charged, except the run-report row if you turned on `includeRunReport`.

**Historical prices?** No. Each run is a snapshot. Schedule runs and keep the datasets.

**Is this affiliated with Woolworths?** No. It's an independent tool, not affiliated with or endorsed by Woolworths Group. It reads the public website.

### Changelog

- woolworths-v1.3.1 (2026-10-03): documentation only (version tag woolworths-v1.3.1, docs). The Reliability section now cites the woolworths-v1.3 release test. No code change; runs still report code version woolworths-v1.3.
- woolworths-v1.3 (2026-10-03): an empty input runs a default search instead of failing (see "Empty input" below).
- First release (2026-10-03): Woolworths only. Search terms, product URLs and bare product IDs.

Builder and compliance notes (lookup order, field mapping, conditions-to-code map): docs/BUILDER_NOTES.md.

### Empty input (woolworths-v1.3, 2026-10-03)

If you give neither `searchTerms` nor `productUrls` (for example an empty `{}` input), the run no longer fails with `INVALID_INPUT`. It searches for `milk 2L` and returns at most 10 products (or your own `maxItems` if you set one), which costs at most $0.02 at 10 products. The status message says the default search was used. Any input with search terms or product URLs works exactly as before.

# Actor input Schema

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

Product keywords, one per entry, e.g. "milk 2L" or "weet-bix". Each term returns matching Woolworths products (24 per page, paged automatically) until maxItems is reached. A product already returned by an earlier input is not repeated. Optional if productUrls is given. Max 100 characters per term. If both searchTerms and productUrls are empty, \["milk 2L"] is searched with maxItems 10 unless you set maxItems.

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

Woolworths product page URLs such as https://www.woolworths.com.au/shop/productdetails/<id>/<slug>, or just the numeric product ID (e.g. "888137"). Each valid product returns one item. Any query string is ignored. Invalid URLs and unknown IDs are listed in the run summary and return no item, so they are not charged. Optional if searchTerms is given. Product URLs are processed before search terms.

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

Upper limit on product items returned across all inputs (1 to 5000). This also caps your cost. Default 100 (10 for an empty input that uses the default search).

## `chains` (type: `array`):

Which supermarket to collect from. Only "woolworths" is supported in this version, and any other value fails with INVALID_INPUT. Default \["woolworths"].

## `includeRunReport` (type: `boolean`):

For testing and debugging. When true, the dataset starts with one extra row (recordType "run-report") holding per-input status, item counts, null counts and errors, followed by the product rows. That extra row counts as one charged result, and it is written even when the run ends FAILED, BLOCKED or ROBOTS_BLOCKED (not for INVALID_INPUT). Per-input status rows are never separate dataset items and are not charged. The same report is always saved free as RUN_SUMMARY in the key-value store. Leave false for normal use so the dataset holds only products.

## Actor input object example

```json
{
  "searchTerms": [
    "milk 2L"
  ],
  "maxItems": 20,
  "chains": [
    "woolworths"
  ],
  "includeRunReport": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (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 2L"
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("dj_orona/au-grocery-prices").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 2L"],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("dj_orona/au-grocery-prices").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 2L"
  ],
  "maxItems": 20
}' |
apify call dj_orona/au-grocery-prices --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dj_orona/au-grocery-prices"
        }
    }
}
```

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/8neW4flrKsm41uN8k/builds/SMO1v1AP7Q5TtaYVV/openapi.json
