# Harvey Norman Australia Scraper - Products & Reviews (`abotapi/harvey-norman-scraper`) Actor

Scrape Harvey Norman Australia products: name, brand, price, was-price / discount, specifications, image gallery, GTIN, department, stock, dimensions, rating and customer reviews. Search by keyword with brand, price and rating filters, or paste product / listing URLs.

- **URL**: https://apify.com/abotapi/harvey-norman-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 product 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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Harvey Norman Australia Scraper — Products & Reviews

Extract product data from **harveynorman.com.au** — Australia's electrical,
computer, furniture and bedding retailer. Search by keyword with filters, or
paste product and listing URLs, and get one clean record per product with an
optional full detail extraction that adds specifications, image gallery,
department, dimensions and customer reviews.

### What you can collect

- **Identity** — canonical title, name, brand, SKU, product id, GTIN, product URL
- **Pricing** — current price, was / original price, discount amount and percent,
  an `isOnSpecial` flag, currency and the promotion label
- **Specials** — scrape the store-wide **Super Deals** or **Clearance** collections
  directly, or paste any sale listing URL
- **Ratings** — average rating and review count on every product
- **Detail (optional)** — full specifications, image gallery, description,
  department, category path, stock status, dimensions
- **Reviews (optional)** — per-review rating, title, body, author, date,
  verified-purchase flag, helpful votes, pros/cons, plus an aggregate summary
  (rating, count, star distribution, recommended count)
- **Resume & recurring updates** (optional) — turn on Incremental mode to get
  only NEW, UPDATED, and REAPPEARED products on every scheduled run, or resume
  one specific interrupted crawl with `resumeFromRunId`

### Modes

#### 🔎 Search

Give one or more keywords (for example `oled tv`, `gaming laptop`). Each keyword
resolves to its best-matching department listing, which is then walked page by
page. Narrow results with **brand**, **minimum / maximum price** and **minimum
rating** filters, and order them with **sort by**.

Pick a **Specials / offers category** to scrape a promotional collection instead
of (or alongside) your keywords:

- **Super Deals** — the store-wide hottest-deals grid
- **Clearance** — the run-out / reduced-to-clear range

Discounted products carry the **was / original price**, the **discount amount and
percent**, an **`isOnSpecial`** flag and the **promotion label** straight from the
listing — no detail fetch required. Products that are not discounted return
`isOnSpecial: false` and `wasPrice: null`; a markdown is never fabricated.

#### 🔗 URL

Paste product URLs (ending in `.html`) to scrape single products, or paste
department / listing URLs to walk multiple pages of products — including any
specials or department-scoped **sale** listing URL (for example a TV sale or
laptop sale page). Both are supported in the same run.

### Detail enrichment & reviews

Turn on **Fetch full product detail + reviews** to open each product page and
collect the full specification set, image gallery, description and an aggregate
review summary plus a `reviews[]` array. Cap the number of reviews per product
with **Max reviews per product** (`0` = all available). With detail off, each
record still carries identity, price and the aggregate rating from the listing.

Products with no reviews simply omit the review fields — they are never faked.

### Example input

```json
{
  "mode": "search",
  "queries": ["oled tv"],
  "brand": "LG",
  "minPrice": 1000,
  "maxPrice": 6000,
  "sortBy": "priceDesc",
  "detailEnrichment": true,
  "maxReviewsPerProduct": 20,
  "maxItems": 50,
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "AU" }
}
```

Scrape the store-wide clearance range with was-prices and reviews:

```json
{
  "mode": "search",
  "specialsCategory": "clearance",
  "detailEnrichment": true,
  "maxReviewsPerProduct": 10,
  "maxItems": 50,
  "proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "AU" }
}
```

### Example output

> Example data below is illustrative.

```json
{
  "productId": 900123,
  "sku": "OLED65EXAMPLE",
  "title": "Acme 65-inch 4K OLED Smart TV",
  "name": "Acme 65-inch 4K OLED Smart TV",
  "brand": "Acme",
  "url": "https://www.harveynorman.com.au/acme-65-inch-4k-oled-smart-tv.html",
  "imageUrl": "https://hnau.imgix.net/media/catalog/product/a/c/acme-65-oled.jpg",
  "price": 3495.0,
  "wasPrice": 3995.0,
  "discountAmount": 500.0,
  "discountPercent": 12.5,
  "isOnSpecial": true,
  "promotion": "Save $500",
  "currency": "AUD",
  "rating": 4.7,
  "reviewCount": 128,
  "brandUrl": "https://www.harveynorman.com.au/brands/acme",
  "description": "Key Features Vivid OLED panel with deep blacks and lifelike colour...",
  "shortDescription": "A 65-inch 4K OLED television with smart features and low-latency gaming.",
  "specifications": {
    "Product Type": "OLED TVs",
    "Screen Size": "65 inch",
    "Resolution": "4K Ultra HD",
    "Barcode (GTIN)": "9312345678900"
  },
  "images": [
    "https://hnau.imgix.net/media/catalog/product/a/c/acme-65-oled-front.jpg",
    "https://hnau.imgix.net/media/catalog/product/a/c/acme-65-oled-side.jpg"
  ],
  "stockStatus": "IN_STOCK",
  "gtin": "9312345678900",
  "department": "Audio Visual",
  "category": "OLED TVs",
  "categoryPath": "tv-blu-ray-home-theatre/tvs-by-type/oled-tvs",
  "dimensions": { "height": 82.6, "depth": 4.5, "width": 144.4, "weight": 22.1, "colour": "Black" },
  "ratingDistribution": { "5": 96, "4": 20, "3": 6, "2": 3, "1": 3 },
  "recommendedCount": 110,
  "reviewsCollected": 2,
  "reviews": [
    {
      "reviewId": "100000001",
      "rating": 5,
      "title": "Stunning picture",
      "body": "Deep blacks and vivid colour, exactly what I hoped for.",
      "author": "SampleReviewer",
      "location": "NSW",
      "date": "2026-06-20T09:15:00.000+00:00",
      "verifiedPurchase": true,
      "isRecommended": true,
      "helpfulCount": 4,
      "notHelpfulCount": 0,
      "isSyndicated": false,
      "pros": ["Picture quality"],
      "cons": null
    }
  ]
}
```

### Tips

- Keep **Max products** modest while you dial in your keywords and filters, then
  raise it for a full pull.
- Australian residential proxy is required for reliable results. On the free
  plan, coverage may be limited and some runs may return no results.
- Use **URL mode** with a department URL to scrape an entire category.
- Set **Max pages per listing** to `0` (the default) to walk the **whole
  catalogue** for a keyword or URL — the run still stops naturally once the
  storefront's own result total is reached or a page repeats with no new
  products, so there is no artificial page cap.
- **Resume a large run:** pass a prior run's ID (or dataset ID) as
  `resumeFromRunId` and the scraper seeds its de-duplication set from that run,
  appending only products it hasn't seen — handy for topping up a
  full-catalogue crawl without re-paying for duplicates.

### Resume & recurring updates

There are two different things here — pick the one that matches what you're doing:

| Need | Use |
| --- | --- |
| A crawl stopped and should continue | `resumeFromRunId` / automatic checkpoint recovery |
| Run the same search every day and receive only changes | `incrementalMode` |
| Keep separate daily campaigns for similar searches | distinct `stateKey` values |
| Run a normal full snapshot | leave both off |

**Resume** (`resumeFromRunId`) continues one specific interrupted or previous large crawl: paste a run ID or dataset ID and this run skips products already collected there, returning only the remaining new products. An automatic same-run checkpoint also protects against platform migrations/Resurrects without any input needed.

**Incremental mode** (`incrementalMode`) is for a schedule (for example, daily): the actor remembers the previous run of the *same* search by itself, so you never paste a run ID. The first run returns everything as `NEW`. Later runs return only `NEW`, `UPDATED`, and `REAPPEARED` products by default — duplicates and unchanged products are suppressed (and not charged). Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned (and billed for). State is isolated per mode/search/URL/specials/brand/sort/price/rating/detail setup automatically; set `stateKey` to name or deliberately share a monitoring campaign. `EXPIRED` is only synthesized once a run fully scans every keyword/URL/specials target (not capped by Max products/Max pages, not a Resume run).

When `incrementalMode` is on, every returned record also carries:

| Field | Description |
|---|---|
| `changeType` | `NEW` | `UPDATED` | `UNCHANGED` | `REAPPEARED` | `EXPIRED` |
| `changedFields` | Top-level fields that changed since last seen; non-empty only for `UPDATED` |
| `firstSeenAt` | When this product was first observed by this monitoring campaign |
| `lastSeenAt` | When this product was last observed |

Scheduled-run example — same search, run daily:

Day 1 (first run ever for this search):

```json
{ "mode": "search", "queries": ["oled tv"], "incrementalMode": true }
```

→ every product comes back with `"changeType": "NEW"`.

Day 2 (the schedule fires again, identical input):

```json
{ "mode": "search", "queries": ["oled tv"], "incrementalMode": true }
```

→ products whose price/stock/specials/etc. changed come back as `"changeType": "UPDATED"` with `changedFields` listing what changed, brand-new products come back as `"changeType": "NEW"`, products that vanished and came back come back as `"changeType": "REAPPEARED"` — and products that are still there, unchanged, are **not** returned at all (suppressed, not charged) unless `emitUnchanged` is on.

### Send results into your apps (optional)

Pipe results straight into the tools you already use via Model Context Protocol
(MCP) connectors — Notion (a rich page per product), Linear, Airtable or Apify.
Authorize a connector under **Apify → Settings → API & Integrations**, then
select it in the input. This never changes the dataset output.

# Actor input Schema

## `mode` (type: `string`):

Choose 'search' to use keywords and filters, or 'url' to scrape specific product or listing URLs you paste below.

## `queries` (type: `array`):

One or more keywords to search, for example 'oled tv' or 'gaming laptop'. Each keyword resolves to its best-matching department listing and is walked page by page.

## `brand` (type: `string`):

Optional. Keep only products whose brand or name matches this value, for example 'LG' or 'Samsung'.

## `minPrice` (type: `integer`):

Optional. Only keep products priced at or above this amount (AUD).

## `maxPrice` (type: `integer`):

Optional. Only keep products priced at or below this amount (AUD).

## `minRating` (type: `integer`):

Optional. Only keep products with an average customer rating at or above this value (1 to 5).

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

Ordering applied to the collected results.

## `specialsCategory` (type: `string`):

Optional. Scrape a Harvey Norman specials collection instead of (or in addition to) your keywords: 'Super Deals' is the store-wide hottest-deals grid and 'Clearance' is the run-out / reduced-to-clear range. Discounted products carry the was-price, discount amount and percent, and the promotion label. Leave empty to search by keyword only. To scrape a department-scoped sale (e.g. TV sale, laptop sale), use URL mode and paste the sale listing URL.

## `urls` (type: `array`):

Product or listing URLs to scrape, for example https://www.harveynorman.com.au/lg-65-inch-g6-evo-ai-4k-oled-smart-tv-2026.html or https://www.harveynorman.com.au/computers-tablets/computers/laptops. Multiple URLs supported.

## `detailEnrichment` (type: `boolean`):

For each product, also open its page to collect the full specification set, image gallery, description, department, dimensions and an aggregate review summary plus a reviews\[] array. Adds one request per product. Off by default (list view returns identity, price and aggregate rating only).

## `maxReviewsPerProduct` (type: `integer`):

Cap on reviews collected per product when detail enrichment is on. Use 0 for all available reviews.

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

Maximum number of products to return across the whole run. Use 0 for unlimited.

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

Cap on result pages walked per keyword / URL. 0 = unlimited (default): walk every page — the run still stops naturally once the storefront's own result total is reached or a page repeats with no new products, so there is no artificial page cap. Set a positive number to cap how deep each individual search goes.

## `resumeFromRunId` (type: `string`):

Optional. ID of a previous run of this actor (or a dataset ID). Products already in that dataset are skipped, so this run returns only NEW products (a delta). Combine both runs' datasets for the full set. Max products then counts only the new products. For recurring daily monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching products as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED products. Turn on "Emit unchanged" or "Emit expired" only when you also want those products returned (and billed). State is kept separately for each distinct search/filter setup (or by State key below).

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from your search/filter/detail settings — different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return products that have not changed since the last run, marked UNCHANGED. This returns — and bills — extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return products that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max products or Max pages capped it, or when Resume was used. This returns — and bills — extra synthetic rows.

## `proxy` (type: `object`):

Apify Proxy (Australian residential) is required for reliable results. On the free plan, coverage may be limited and some runs may return no results.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a rich page-per-item export; other connectors get a best-effort write/digest. Leave empty to skip; never changes the dataset output. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "oled tv"
  ],
  "sortBy": "relevance",
  "urls": [
    "https://www.harveynorman.com.au/computers-tablets/computers/laptops"
  ],
  "detailEnrichment": false,
  "maxReviewsPerProduct": 20,
  "maxItems": 20,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "AU"
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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 = {
    "mode": "search",
    "queries": [
        "oled tv"
    ],
    "urls": [
        "https://www.harveynorman.com.au/computers-tablets/computers/laptops"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "AU"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/harvey-norman-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 = {
    "mode": "search",
    "queries": ["oled tv"],
    "urls": ["https://www.harveynorman.com.au/computers-tablets/computers/laptops"],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "AU",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/harvey-norman-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "search",
  "queries": [
    "oled tv"
  ],
  "urls": [
    "https://www.harveynorman.com.au/computers-tablets/computers/laptops"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "AU"
  }
}' |
apify call abotapi/harvey-norman-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=abotapi/harvey-norman-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Xy8YanyYVzp9BS2h5/builds/tAlhT9kB5igW3AazZ/openapi.json
