# Coles Price Tracker - Price Drops, Specials & Stock by Category (`neverempty/coles-price-change-monitor`) Actor

Track Coles prices by category: paste coles.com.au category URLs and get every product's price, was price, special, multi-buy offer, unit price and stock. Monitor mode returns only price drops, new specials and newly listed products since the last run. Reliable reads with automatic retries.

- **URL**: https://apify.com/neverempty/coles-price-change-monitor.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (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.99 / 1,000 product returneds

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

## Coles Price Tracker - Price Drops, Specials & Stock by Category

Track Coles prices by category: paste any coles.com.au category URL (for example **Milk**, **Bakery** or the **Full Cream Milk** aisle) and get every product with its **price, was price, special, multi-buy offer, unit price and stock**. Turn on monitor mode and each later run returns **only the price drops, new specials and newly listed products** since the last run, with the previous price next to the new one.

Use it as a Coles price API for a spreadsheet, a price comparison site, a deal alert bot or category and promotion research.

### What you get

- **Every product in a category, in flat rows.** Name, brand, size, price, was price, saving, `onSpecial`, the special's label (`1/2 Price`, `40% Off`), multi-buy offers (`Pick any 2 for $10.50`), unit price (`$1.65/ 1L`), in stock or not, aisle, product URL and image. Departments, categories and aisles all work, up to 200 per run.
- **Coles specials alerts without the noise.** Monitor mode remembers each category. Later runs return only:
  - `price-drop`: the price went down (with `previousPrice`),
  - `new-special`: the product went on special,
  - `new-in-category`: a product Coles has just started listing in that category,
  - `price-increase` too, if you switch it on.
    A category where nothing changed costs one small `category-checked` event instead of a row per product.
- **Reads that come back.** Coles answers busy readers with a check page. This Actor reads every page on a fresh IP address and asks again on another one when that happens, so a scheduled run returns the category instead of an empty dataset.
- **The whole category, not most of it.** Coles moves products between pages while you page through a category (the number of sponsored slots changes from page to page), so a simple page-by-page read skips some products and repeats others. This Actor counts what it found against the total Coles reports and reads the pages again until they match. In our test run on the Milk category it returned 198 of the 198 products Coles listed.
- **Only the category you asked for.** Every product is checked against the category path Coles files it under, and products from a different page or category are not returned. Each product appears once per category, even when Coles also shows it in a sponsored slot.
- **No wrong price is better than a wrong price.** A product listed without a price comes back as a free `no-price` row. A category that cannot be read comes back as a free row that says why.

### Questions people ask

**How do I track Coles prices?**
Add the category URLs you care about, switch on `onlyChanges` and schedule the Actor (for example daily). The first run records the starting point for free; every later run lists only what dropped, went on special or was newly listed.

**Is there a Coles API for product prices?**
This Actor works as one: run it through the Apify API and you get JSON, CSV or Excel with one row per product, from the same category pages a shopper sees.

**How do I find Coles half price specials in a category?**
Run the category and filter `priceDescription` = `1/2 Price` (or `onSpecial` = true for every special). In monitor mode, `new-special` rows tell you when a product goes on special.

**Which categories can I use?**
Any `https://www.coles.com.au/browse/...` page: a department (`/browse/bakery`), a category (`/browse/dairy-eggs-fridge/milk`) or an aisle (`/browse/dairy-eggs-fridge/milk/full-cream-milk`). You can also give just the path, like `dairy-eggs-fridge/milk`.

### Input

```json
{
  "categoryUrls": [
    "https://www.coles.com.au/browse/dairy-eggs-fridge/milk",
    "https://www.coles.com.au/browse/bakery/packaged-breads"
  ],
  "onlyChanges": true
}
```

| Field | What it does |
|---|---|
| `categoryUrls` | Coles category pages, up to 200 per run. Filters and page numbers in the URL are ignored; the whole category is read from page 1. Empty = the example aisle Full Cream Milk. |
| `maxItemsPerCategory` | Most products per category in a normal run. Empty = every product on the pages read. |
| `maxPagesPerCategory` | Most pages read per category (48 products each), 1 to 150. Default 25 (1,200 products). |
| `onlyChanges` | Monitor mode. First run: records the starting point, free, no product rows. Later runs: only price drops, new specials and newly listed products. |
| `alertOnPriceIncrease` | Monitor mode also returns price increases. |
| `watchName` | Optional. A separate memory per name (for example one per schedule). |
| `resetMonitoringState` | Forget what this watch remembers and start again. |
| `maxConcurrency` | Categories read at the same time, 1 to 4 (default 2). |
| `requestTimeoutSecs` | Seconds to wait for one page, 5 to 60 (default 30). |

### Output

One row per product:

```json
{
  "status": "ok",
  "categoryPath": "dairy-eggs-fridge/milk",
  "page": 1,
  "position": 1,
  "sponsored": false,
  "productId": "1054581",
  "name": "Protein Smoothie Chocolate",
  "brand": "Dairy Farmers",
  "size": "400mL",
  "price": 3.3,
  "wasPrice": 4.55,
  "saveAmount": 1.25,
  "onSpecial": true,
  "promotionType": "SPECIAL",
  "unitPrice": 8.25,
  "unitPriceText": "$8.25/ 1L",
  "inStock": true,
  "department": "Dairy, Eggs & Fridge",
  "category": "Milk",
  "aisle": "Flavoured Milk",
  "productUrl": "https://www.coles.com.au/product/dairy-farmers-protein-smoothie-chocolate-400ml-1054581",
  "imageUrl": "https://cdn.productimages.coles.com.au/productimages/1/1054581.jpg",
  "categoryTotalProducts": 198,
  "changeType": null
}
```

| Column | Meaning |
|---|---|
| `price` | The price shown on the category page, in Australian dollars. For products sold by weight (`soldByWeight` true) it is the price of the approximate pack size in `size`. |
| `wasPrice`, `saveAmount`, `savePercent` | The price before the special and the saving, when Coles shows them. |
| `onSpecial`, `specialType`, `priceDescription` | `onSpecial` is true when the product carries Coles's SPECIAL label (including multi-buy and specials without a was price). `specialType` is `PERCENT_OFF` or `MULTI_SAVE`; `priceDescription` is the label text such as `1/2 Price`. |
| `offerDescription`, `multiBuyMinQuantity`, `multiBuyPriceEach` | Multi-buy offers such as `Pick any 2 for $10.50`, the quantity, and the price per item when you buy that many. |
| `onlineOnlySpecial` | True when the special is an online-only price. |
| `unitPrice`, `unitPriceText`, `unitOfMeasure` | Unit price as a number, as Coles prints it, and the unit it is per (`1l`, `100g`, `1ea`). |
| `inStock`, `availableQuantity`, `retailLimit` | In stock or not, the quantity Coles reports as available, and the most you can buy in one order. |
| `department`, `category`, `aisle` | Where Coles files the product inside the category you asked for. |
| `page`, `position`, `sponsored` | The category page the product was read from and its place on that page. Products that appeared only in a sponsored slot come last with `sponsored: true` and no position. |
| `storeId` | The Coles store whose prices the page shows (the store coles.com.au uses when no store is chosen). |
| `changeType`, `changeTypes`, `previousPrice`, `previousWasPrice`, `previousOnSpecial`, `previousCheckedAt` | Monitor mode only: what changed and the remembered values. |
| `status`, `note` | `ok` for a product row (charged). Every other status is a free row that says why. |

Other statuses (all free): `no-price`, `empty-category`, `not-found`, `moved-off-site`, `blocked`, `pages-not-read`, `robots-disallowed`, `bad-input`, `baseline-recorded`, `no-change`, `budget-reached`.

### Pricing

Pay per event:

- `product-returned`: one product row with a price.
- `category-checked`: monitor mode only, one per category on a run where nothing in that category changed.

The first monitor-mode run of a category, free rows, and categories or pages that cannot be read are not charged. If you set a maximum charge for a run, the Actor stops before it and the products it could not return are returned on the next run.

### Good to know

- Prices are the online prices of the default store coles.com.au shows when no store is chosen (`storeId`). In-store prices can differ by store.
- In monitor mode a category is compared as a whole. If a category has more pages than `maxPagesPerCategory`, products seen for the first time are remembered without being reported as newly listed; price drops and new specials are still reported. Use aisle URLs or raise `maxPagesPerCategory` for very large departments.
- Coles's robots.txt is followed: search pages, the specials listing pages and filter URLs are not read, and filters in a pasted URL are ignored.
- Liquor categories are read from Coles's Liquorland pages on coles.com.au (`/browse/liquor` is sent to `/browse/liquorland`). Categories Coles shows only in stores, such as tobacco, have no products to return and come back as a free `empty-category` row.
- When Coles has moved a category to a new address, the new address is read and `categoryPath` shows it.
- Products are listed in Coles's default order. Each page is read on a fresh Apify Proxy IP address; if Coles still shows its check page on every try, that category comes back as a free `blocked` row. This Actor does not solve check pages.

### Thanks for using this Actor

We build these tools for people who run them every day, and we improve them from what users tell us.

- **Missing a field, or need another filter?** Tell us in the **Issues** tab. If the data is there, we add it.
- **Found a bug or a wrong value?** Post the run ID in the **Issues** tab. Wrong data is the thing we fix first.

If this Actor saved you time, a short review helps other people find it.

# Actor input Schema

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

Coles category pages, one per line (up to 200 per run), copied from the browser address bar, like https://www.coles.com.au/browse/dairy-eggs-fridge/milk. A department (/browse/bakery), a category or an aisle (/browse/dairy-eggs-fridge/milk/full-cream-milk) all work, and so does just the path dairy-eggs-fridge/milk. Filters and page numbers in the URL are ignored; the whole category is read from page 1. If you leave this empty, the example aisle Full Cream Milk is read.

## `maxItemsPerCategory` (type: `integer`):

Most products returned for one category in a normal run (Coles shows 48 per page). Empty = every product on the pages read. Not used in monitor mode, which compares the whole category.

## `maxPagesPerCategory` (type: `integer`):

How many category pages (48 products each) are read at most, 1 to 150. Empty = 25 pages (1,200 products). Most categories and aisles fit; large departments such as Pantry have more than 100 pages.

## `onlyChanges` (type: `boolean`):

On: the first run records the price and special of every product in each category as the starting point (free, no product rows). Later runs with the same categories return only products whose price dropped, that went on special, or that are newly listed, with the previous price. A category where nothing changed costs one small category-checked event. Off or empty: return every product.

## `alertOnPriceIncrease` (type: `boolean`):

On: monitor mode also returns products whose price went up (changeType price-increase). Off or empty: price increases are remembered but not returned.

## `watchName` (type: `string`):

Keeps a separate memory per name, so two schedules with different category lists do not mix. Letters, digits, dot, dash and underscore, up to 40 characters. Empty means the watch called default.

## `resetMonitoringState` (type: `boolean`):

Monitor mode only: forget the remembered prices of this watch name first, so this run records a new starting point. Off or empty: keep the memory.

## `maxConcurrency` (type: `integer`):

How many categories are read in parallel (1 to 4); the pages of one category are read one after another. Empty = 2.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for one category page before asking again (5 to 60). Empty = 30.

## Actor input object example

```json
{
  "categoryUrls": [
    "https://www.coles.com.au/browse/dairy-eggs-fridge/milk/full-cream-milk"
  ],
  "maxPagesPerCategory": 25,
  "onlyChanges": false,
  "alertOnPriceIncrease": false,
  "resetMonitoringState": false,
  "maxConcurrency": 2,
  "requestTimeoutSecs": 30
}
```

# Actor output Schema

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

One row per Coles product in the categories you gave: name, brand, size, price, was price, special and multi-buy offer, unit price, stock and product URL. In monitor mode only price drops, new specials and newly listed products, with the previous price. A category that cannot be read comes back as a free row that says why.

# 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 = {
    "categoryUrls": [
        "https://www.coles.com.au/browse/dairy-eggs-fridge/milk/full-cream-milk"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/coles-price-change-monitor").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 = { "categoryUrls": ["https://www.coles.com.au/browse/dairy-eggs-fridge/milk/full-cream-milk"] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/coles-price-change-monitor").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 '{
  "categoryUrls": [
    "https://www.coles.com.au/browse/dairy-eggs-fridge/milk/full-cream-milk"
  ]
}' |
apify call neverempty/coles-price-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/coles-price-change-monitor"
        }
    }
}
```

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/bbqhOS4xYrjMShwpI/builds/5ePNFJLQPAPGATcnL/openapi.json
