# JB Hi-Fi Product Scraper & Price Tracker (Australia) (`superslowsloth/jbhifi-products`) Actor

JB Hi-Fi scraper: the newest 25,000 products or any collection, filtered by brand (APPLE, SAMSUNG, SONY...), sale and publish time. One row per product: AUD price range, price before markdown, per-seller offers and SKU, stock tag, photo, timestamps. For JB Hi-Fi price drop and new-product watch.

- **URL**: https://apify.com/superslowsloth/jbhifi-products.md
- **Developed by:** [Superslow Sloth](https://apify.com/superslowsloth) (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.28 / 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

## JB Hi-Fi Product Scraper & Price Tracker (Australia)

A **JB Hi-Fi scraper** and **JB Hi-Fi price tracker** for
[www.jbhifi.com.au](https://www.jbhifi.com.au), Australia's largest home
entertainment retailer. It reads the catalogue newest-published first and
returns one flat row per product: title, brand (`APPLE`, `SAMSUNG`, `SONY`,
`LEGO`), the price range in **AUD**, the price before markdown and the discount,
whether it can be bought, the shop's stock tag (`InStock`, `LimitedStock`,
`AwaitingStock`), and every offer as a variant with its own SKU, price and stock
flag. Filter by brand, by **sale** and by **publish time** (`24 hours`,
`3 days`) and schedule it to get a **new-product alert**, or run it daily and
compare rows by `handle` to watch each product's **price drop**.

No account, no API key, no login, no browser. JB Hi-Fi's category pages are
rendered in the browser and hold no products in the HTML, but the shop's open
product feed does: one request returns 250 products. JB Hi-Fi published about
800 products in the 24 hours to 2026-10-27, so a 24-hour window is four
requests.

### Input

| Field | Notes |
|---|---|
| `brands` | Return only products by these brands, spelled as JB Hi-Fi spells them in `vendor`: `APPLE`, `SAMSUNG`, `SONY`, `GOOGLE`, `LEGO`, `DELL`, `HP`, `ASUS`. Case and extra spaces are ignored; otherwise the match is exact. Leave empty for every brand. |
| `onlyOnSale` | Return only products where at least one variant is priced below its price before markdown (`on_sale`). Only about 0.7% of products carry one (see below), so this reads many pages to find a few rows. |
| `publishedAfter` | Return only products that went live after this moment: a span back from now (`24 hours`, `3 days`, `90 minutes`, `1 week`), a date (`2026-10-16`, midnight UTC) or a date-time (`2026-10-16T12:00:00Z`, no offset means UTC). This is the product's **publish time** (`published_at`), not the record's creation time (`created_at`). On the whole catalogue the run stops at the first older product, since the feed is newest-published first. Leave empty for no cut-off. |
| `collections` | JB Hi-Fi collections to read instead of the whole catalogue, one per line: the name from the address (`android-phones_3binxgvkinsnudz6imeoau`) or the link (`https://www.jbhifi.com.au/collections/android-phones_3binxgvkinsnudz6imeoau`). The shop's category pages (`televisions`, `sale`, `apple-iphone`) are not collections here and fail as unknown. A collection is in the shop's own order, **not** newest first, so `publishedAfter` filters it but cannot stop early. Leave empty to read the whole catalogue, newest first. |
| `maxItems` | Products to return per collection, or for the whole catalogue (default 250, up to 25,000). 250 per page. |
| `proxyConfiguration` | Apify datacenter proxy by default. |

Filtered products are read but not returned and not charged, and do not count
toward `maxItems`.

Example input, products that went live on JB Hi-Fi in the last day:

```json
{
  "publishedAfter": "24 hours",
  "maxItems": 1000
}
```

Example input, every Apple product among the newest 25,000, to snapshot prices:

```json
{
  "brands": ["APPLE"],
  "publishedAfter": "",
  "maxItems": 3000
}
```

Example input, everything currently marked down:

```json
{
  "onlyOnSale": true,
  "publishedAfter": "",
  "maxItems": 500
}
```

### Output

Prices are numbers in Australian dollars; `currency` is `AUD`. The feed is not
re-priced by the visitor's country: the same 250 products carried the same
prices from exits in the United States, Colombia, Brazil, Saudi Arabia and
Japan, and under every `localization` cookie tried (2026-10-27). A field the
shop did not send is `null`, never `0` or `""`.

| Field | Notes |
|---|---|
| `query`, `position` | The shop URL (whole catalogue) or collection URL that first found the product, and its place in that list (1 is newest on the catalogue, 250 per page). |
| `store` | `www.jbhifi.com.au`. |
| `product_id`, `handle`, `url` | Shopify's product id (a string), the URL handle and the product link. Use `handle` or `product_id` to follow one product between runs. |
| `title`, `vendor`, `product_type` | `Apple iPhone 17 Pro Max 512GB (Silver)`, `APPLE`, `COMMUNICATIONS`. `vendor` is the brand in the shop's upper case. `product_type` is one of 19: `MOVIES`, `IT`, `ACCESSORIES`, `TELCO SERVICES`, `MUSIC`, `Wearables & Outdoor`, `CAMERAS`, `COMPUTERS`, `AUDIO`, `SMALL APPLIANCES`... |
| `price`, `price_max` | The lowest and highest current variant price. On a marketplace product (below) that is the cheapest and dearest seller's price. |
| `original_price`, `original_price_max`, `on_sale`, `discount_percent` | Per variant its `compare_at_price` when that is above its price, otherwise its price. `on_sale` is true when at least one variant is reduced, and `discount_percent` is the largest reduction, `(1 - price / compare_at_price) * 100`, one decimal place, worked out from the prices. **Null when nothing is reduced**, never 0. |
| `available`, `variant_count`, `available_variant_count` | Whether any variant can be bought now, how many variants there are, and how many can be bought. |
| `tags` | `Brand:Apple` and a stock-status tag: `InStock`, `LimitedStock`, `AwaitingStock`, `NoLongerAvailable`, `TemporarilyUnavailable`, `ComingSoon`. The status tag is the only stock signal finer than `available`. |
| `option_names` | `["Title"]` on JB Hi-Fi's own stock, `["Seller"]` on a marketplace product. |
| `variants` | One entry per offer: `variant_id`, `title` (the seller's name on a marketplace product, `Default Title` on JB Hi-Fi's own), `sku`, `price`, `compare_at_price`, `available`, `options`. |
| `published_at` | When the product went live, ISO 8601 with the shop's offset. What the feed is ordered by and what `publishedAfter` compares against. A relisted product gets a new one. |
| `created_at`, `updated_at` | When the record was created (can be a year older than `published_at`) and last changed. The shop rewrites `updated_at` in bulk, so it is not a price-change time. |
| `image_url`, `image_count` | The first photo on cdn.shopify.com, as sent, and how many photos there are. |

Example row (real values, 2026-10-27, exactly as the actor returns it):

```json
{
  "query": "https://www.jbhifi.com.au",
  "position": 4,
  "store": "www.jbhifi.com.au",
  "product_id": "7334830899401",
  "title": "Apple iPhone 17 Pro Max 512GB (Silver)",
  "handle": "apple-iphone-17-pro-max-512gb-silver",
  "url": "https://www.jbhifi.com.au/products/apple-iphone-17-pro-max-512gb-silver",
  "vendor": "APPLE",
  "product_type": "COMMUNICATIONS",
  "tags": [
    "Brand:Apple",
    "LimitedStock"
  ],
  "created_at": "2025-09-10T06:38:03+10:00",
  "published_at": "2026-10-26T22:02:24+11:00",
  "updated_at": "2026-10-27T07:04:42+11:00",
  "image_url": "https://cdn.shopify.com/s/files/1/0024/9803/5810/files/816127-Product-0-I-638930469805142860.jpg?v=1757904007",
  "image_count": 18,
  "currency": "AUD",
  "price": 2399.0,
  "price_max": 2399.0,
  "original_price": 2599.0,
  "original_price_max": 2599.0,
  "on_sale": true,
  "discount_percent": 7.7,
  "available": true,
  "variant_count": 1,
  "available_variant_count": 1,
  "option_names": [
    "Title"
  ],
  "variants": [
    {
      "variant_id": "40426204135625",
      "title": "Default Title",
      "sku": "816127",
      "price": 2399.0,
      "compare_at_price": 2599.0,
      "available": true,
      "options": [
        "Default Title"
      ]
    }
  ]
}
```

#### Honest nulls and what is not here

- **Most products are not JB Hi-Fi's own stock.** 78% of the newest 25,000
  (19,519) are marketplace listings: `option_names` is `["Seller"]` and each
  variant is one seller's offer (153 sellers; Gameology, Hobbyco, Green Gadgets,
  Desky and Phonnic hold the most). The seller is the variant's `title`. 936
  products have more than one seller, and the `sku` is the same on each. The
  other 5,481 are JB Hi-Fi's own stock, with one `Default Title` variant.
- **Markdowns are rare.** Only 169 of 25,000 products (0.7%) send a price
  before markdown, all of them JB Hi-Fi's own stock and none of the
  marketplace listings, and every one is above the price. For the rest,
  `discount_percent` is null and the way to see a price fall is to compare
  `price` between two runs.
- **No ratings, reviews or stock counts.** The catalogue feed carries none.
  `available` and the stock tag say what the shop shows, nothing more.
- **No product lacks a price.** No product in the 25,000 is priced `0.00`, has
  no photo, no product type or a variant without a `sku`.

### Pricing

Pay per event:

- **$0.002** per run (`actor-start`), charged only after your input is
  validated, so a run that fails on bad input costs nothing.
- **$0.00028** per product row written (`product-scraped`), i.e. $0.28 per
  1,000 products. A row that is not written is not charged: products filtered
  by `brands`, `onlyOnSale` or `publishedAfter`, and repeats another collection
  already returned, are free.

### How it fetches

It reads `/products.json`, the public catalogue feed every Shopify shop serves
(and `/collections/<name>/products.json` for a collection). Measured
2026-10-27: HTTP 200 JSON, 1.03 MB per 250 products (4.1 KB a product), no key
or login needed, 100 consecutive pages without a refusal from Apify datacenter
addresses. A refusal (403, 429, 430, 5xx), a 200 that is not the feed, or a
dropped connection is treated as the address being blocked, and the actor
rotates to a fresh one and retries.

Things worth knowing, all measured on 2026-10-27:

- **Newest-published first.** `published_at` never increased down all 100
  pages (25,000 distinct products, 2026-10-27 06:31 back to 2026-08-21 15:54,
  about 66 days). `created_at` is not in order, which is why the cut-off is on
  `published_at`. About 800 products went live in a day and 4,200 in a week.
- **At most 25,000 products per list.** Shopify refuses any page beyond
  `page * 250 = 25,000` with HTTP 400. JB Hi-Fi lists more than that
  (`/meta.json` says 25,001), so the whole catalogue always ends at the window,
  about two months back, and a run that reads to it without having returned
  `maxItems` rows logs the list as capped. Products published earlier than that
  are reachable only through a collection.
- **Filters on the feed are ignored.** `?vendor=`, `?product_type=`, `?q=` and
  `?order=` return the same page as sending nothing, so `brands` are matched on
  the rows. A rare brand on the whole catalogue therefore reads up to 100 pages
  (about 100 MB, about four minutes); a short `publishedAfter` window stops long
  before that.
- **Collections are small and hand-made.** The shop lists 13,987 collection
  handles, 778 of them with any product and none with more than 18; the category
  names a shopper types (`televisions`, `sale`, `apple-iphone`) are not
  collections. A collection's count includes unpublished products
  (`kids-sci-fi-movies` said 18, its feed held 4). They do reach older products:
  `android-phones_3binxgvkinsnudz6imeoau` returned four phones published between
  February 2025 and July 2026, all four on sale (7.5% to 30.0% off).
- **An unknown collection looks empty.** The feed answers an unknown name with
  an empty list; the actor asks the collection's own record, and a name that
  does not exist fails instead of being reported as empty.

If JB Hi-Fi changes its feed, the run fails with a message instead of returning
empty rows.

# Actor input Schema

## `brands` (type: `array`):

Return only products by these brands, one per line, spelled as JB Hi-Fi spells them in the vendor field: APPLE, SAMSUNG, SONY, GOOGLE, LEGO, DELL, HP, ASUS. Matching ignores case and extra spaces but is otherwise exact. Other products are read but not returned and not charged. The shop has no filter on its feed, so a brand is found by reading pages, up to 100 of them (about 100 MB) for a rare brand on the whole catalogue. Leave empty for every brand.

## `onlyOnSale` (type: `boolean`):

Return only products where at least one variant is priced below its price before markdown (the on_sale field). JB Hi-Fi sends a price before markdown for about 0.7% of its products (169 of the newest 25,000, all of them its own stock, none of the marketplace sellers'), so this reads many pages to find a few rows. Other products are read but not returned, not charged, and do not count toward Max products.

## `publishedAfter` (type: `string`):

Return only products that went live on JB Hi-Fi after this moment: a span back from now (24 hours, 3 days, 90 minutes, 1 week), a date (2026-10-16, midnight UTC) or a date-time (2026-10-16T12:00:00Z). This is the product's publish time (published_at), not the record's creation time (created_at), which can be months older. On the whole catalogue the run stops at the first older product, because the feed is newest-published first, so a short window is fast and cheap (about 800 products a day, 4,200 a week). Leave empty for no cut-off.

## `collections` (type: `array`):

JB Hi-Fi collections to read instead of the whole catalogue, one per line: the name from the shop's address (android-phones_3binxgvkinsnudz6imeoau) or the whole link (https://www.jbhifi.com.au/collections/android-phones_3binxgvkinsnudz6imeoau). The shop's category pages (televisions, sale, apple-iphone) are not collections and are refused as unknown; the collections that exist are small hand-curated lists, 4 to 18 products. A collection is in the shop's own order, not newest first, so Published after filters it without stopping early. A collection reaches older products than the newest 25,000. Leave empty to read the whole catalogue, newest first.

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

Return at most this many products for the catalogue or for each collection. The feed holds 250 per page. Up to 25,000 are accepted, which is the most Shopify serves for any one list; the whole catalogue is longer than that, so a full read is capped at its newest 25,000.

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

Use Apify's automatic datacenter proxy (the default). A refused request is retried from a fresh address.

## Actor input object example

```json
{
  "brands": [],
  "onlyOnSale": false,
  "publishedAfter": "24 hours",
  "collections": [],
  "maxItems": 250,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `items` (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 = {
    "brands": [],
    "publishedAfter": "24 hours",
    "collections": [],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("superslowsloth/jbhifi-products").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 = {
    "brands": [],
    "publishedAfter": "24 hours",
    "collections": [],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("superslowsloth/jbhifi-products").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 '{
  "brands": [],
  "publishedAfter": "24 hours",
  "collections": [],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call superslowsloth/jbhifi-products --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,superslowsloth/jbhifi-products"
        }
    }
}
```

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/pOwPMQHeejk7AWgd1/builds/K6qYWfa9k7Y21Jm6i/openapi.json
