# ALDI AU Scraper - Groceries, Special Buys & Prices (`abotapi/aldi-com-au-scraper`) Actor

Scrape ALDI Australia (aldi.com.au) products: name, brand, price, was-price, savings, unit price, size, category, images, availability and Special Buys on-sale date, plus full detail (ingredients, allergens, nutrition). Search by keyword or category, filter by brand/theme, or paste any ALDI URL.

- **URL**: https://apify.com/abotapi/aldi-com-au-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 12 total users, 5 monthly users, 100.0% runs succeeded, 2 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/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

## ALDI Australia Scraper: Prices, Special Buys & Full Product Detail

ALDI Australia Scraper turns aldi.com.au into structured product data. Get product name, canonical title, brand, current price, numeric was-price with savings amount and percent, unit (comparison) pricing, selling size, full category path, image gallery, availability and Special Buys on-sale date on every record. Search by keyword or category, pull the Special Buys, Lower Prices or Super Savers range directly, or paste any ALDI product, category or search link. Switch on detail enrichment for the full description, ingredients, allergen statements, nutritional claims, storage and usage instructions and country of origin, then export to JSON, CSV or Excel, or read results straight from the API.

### Why This Scraper?

- **Four ways to collect.** Pull a Special Buys, Lower Prices or Super Savers range directly, search by keyword, list any grocery category, or paste ALDI product, category or search links.
- **Real pricing detail.** Numeric was-price with savings amount and percent alongside the pre-formatted display strings, plus unit (comparison) pricing on every record.
- **Optional full detail.** Description, ingredients, allergen statements, nutritional claims, storage and usage instructions and country of origin, switched on per run.
- **Department control.** Filter by brand or theme, and drop whole departments from your output by each product's own reported category, even when a promo range cross-lists it from elsewhere.
- **Built for schedules.** Incremental mode returns only new, updated, reappeared and expired products on recurring runs, and continuing one interrupted crawl never re-collects or re-charges what it already gathered.
- **Ready-to-use media and links.** A direct product URL, its category page URL, a main image and the full ready-to-download image gallery on every record.
- **Special Buys themes and calendar.** Every Special Buys product is tagged with its ALDI theme (BBQ, Gardening, Christmas...), and each run that touches Special Buys saves ALDI's current on-sale calendar with the themes of every date.

### Use Cases

- **Price monitoring teams:** track ALDI's everyday-low prices and Special Buys week over week to catch changes before shoppers do.
- **Grocery comparison sites:** combine numeric price, unit price and savings percent across categories to power a comparison engine.
- **Range and promo research:** pull Special Buys, Lower Prices or Super Savers directly to see what ALDI is currently promoting.
- **Nutrition and compliance review:** collect ingredients, allergen statements and nutritional claims for a whole category at once with detail enrichment on.
- **Catalogue history:** run Incremental mode on a schedule to build a running record of price changes, new listings and discontinued products.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live listing.

| Field | Example |
| --- | --- |
| `sku` | `"000000000011112222"` (the canonical per-product key; use this to dedupe/join, not `abstractSku`) |
| `abstractSku` | `"000000000011112222A"` (groups sibling variants that are still different products, e.g. different flavours or sizes of the same base item) |
| `name` / `title` | `"Full Cream Milk 2L"` / `"SAMPLE BRAND Full Cream Milk 2L"` (`title` prefixes the brand when it isn't already part of the name) |
| `brand` | `"SAMPLE BRAND"` |
| `price` / `priceDisplay` | `7.29` / `"$7.29"` |
| `wasPrice` / `wasPriceDisplay` | `8.99` / `"$8.99"` (populated only while the product is actually discounted) |
| `savingsAmount` / `savingsPercent` | `1.7` / `18.9` |
| `savingsDisplay` / `promoLabel` | `"Save $1.70"` / `"Save $1.70"` |
| `onSale` | `true` |
| `unitPrice` / `unitPriceDisplay` | `3.65` / `"$3.65 per 1 L"` |
| `unitOfMeasure` / `unitMeasure` | `"L"` / `"1L"` (parsed from the same display string as `unitPrice`; always read the two together, never `unitPrice` alone) |
| `perUnitQuantity` / `perUnitQuantityUnit` | `0.18` / `"kg"` (loose-produce configured weight, not a price; empty on packaged items like this one) |
| `sellingSize` | `"2 L"` |
| `categoryPath` / `category` / `categoryUrl` | `"Dairy, Eggs & Fridge > Milk"` / `"Milk"` / `"https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160"` (the listing page of the product's own category) |
| `categories` | `[{"id": "960000000", "name": "Dairy, Eggs & Fridge", "path": "dairy-eggs-fridge"}, {"id": "1111111160", "name": "Milk", "path": "dairy-eggs-fridge/milk"}]` |
| `promotions` / `themes` | `["Lower Prices"]` / `["Pet Essentials"]` (the promotional ranges the product sits on: Limited Time Only, Lower Prices, Super Savers; and its ALDI theme, e.g. a Special Buys theme. `[]` when none; `themes` is left empty when ALDI's theme list for that search is too long to read completely) |
| `position` / `input` / `inputType` | `3` / `"milk"` / `"searchTerm"` (rank in ALDI's own result list, and which search term, category, Special Buys date or URL found it) |
| `scrapedAt` | `"2026-10-06T01:02:03Z"` |
| `isSpecialBuy` / `onSaleDateDisplay` | `true` / `"On Sale Sat 5 Jul"` (set whenever the product carries a Special Buys on-sale date) |
| `images` / `imagesCount` / `imageUrl` | `["https://cdn.aldi.com.au/products/full-cream-milk-2l.jpg"]` / `1` / `"https://cdn.aldi.com.au/products/full-cream-milk-2l.jpg"` (`imageUrl` is the main image on its own, ready for a table or spreadsheet) |
| `url` | `"https://www.aldi.com.au/product/full-cream-milk-2l-000000000011112222"` |
| `description` | `"Full product description appears here when Fetch product details is on."` |
| `ingredients` | `"Full-cream milk."` |
| `allergenStatements` | `["Contains: Milk"]` (derived from `nutritionalClaims`; ALDI's own `allergens` array is always empty) |
| `changeType` / `changedFields` / `previousValues` | `"UPDATED"` / `["price", "wasPrice"]` / `{"price": 7.99, "wasPrice": null}` (Incremental mode only; `previousValues` is each changed field's value on the previous run) |

Every record also carries the product's own top-level `department`, a `notForSale` flag, currency and currency symbol, quantity/weight metadata, bottle deposit and fee text where they apply, age restriction and alcohol content on restricted items, badges, images-gallery detail such as usage/storage instructions and warnings once detail enrichment is on, and a `detailFetched` flag. `perUnitPrice` is always empty: it is a deprecated field kept for compatibility; the honest per-kg price is `unitPrice` read together with `unitMeasure`. Incremental mode also adds `firstSeenAt` / `lastSeenAt` timestamps.

### How to Use

1. Pick a **mode**: `search` (keyword, category or a Specials range) or `url` (paste ALDI links).
2. In search mode, add search terms and/or categories, optionally pick a Specials/offers range, and filter by brand or theme.
3. Turn on **Fetch product details** if you need the full description, ingredients and allergen data, and set **Max products** to control run size and cost.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Search by keyword:**

```json
{
  "mode": "search",
  "searchTerms": ["milk", "coffee"],
  "maxItems": 50
}
```

**Scrape a Specials range directly, with full detail:**

```json
{
  "mode": "search",
  "specialsCategory": "special-buys",
  "fetchDetails": true,
  "maxItems": 100
}
```

**Paste URLs (every supported shape):**

```json
{
  "mode": "url",
  "urls": [
    "https://www.aldi.com.au/product/sample-product-000000000099998888",
    "https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160",
    "https://www.aldi.com.au/products",
    "https://www.aldi.com.au/special-buys/2026-10-07?theme=Pet+Essentials",
    "https://www.aldi.com.au/special-buys",
    "https://www.aldi.com.au/results?q=milk",
    "https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160?brand-name=FARMDALE&sort=price_asc"
  ]
}
```

**Recurring monitoring, with a full uncapped scan so expired products can be detected:**

```json
{
  "mode": "search",
  "categories": ["Dairy, Eggs & Fridge"],
  "incrementalMode": true,
  "emitExpired": true,
  "maxItems": 0
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/aldi-com-au-scraper").call(run_input={"mode": "search", "searchTerms": ["milk"], "maxItems": 50})
for product in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(product["name"], product["price"])
```

JavaScript:

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/aldi-com-au-scraper').call({ mode: 'search', searchTerms: ['milk'], maxItems: 50 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Every supported URL shape (url mode)

A pasted product URL always returns that single product with its full detail page, regardless of **Fetch product details**; there is no way to get a catalogue-only row for a single product URL. The detail-enrichment charge for it still only applies when **Fetch product details** is on, same as any other record. Every other shape below behaves like the equivalent search-mode run: it returns the full result set up to **Max products** and respects **Fetch product details**.

| Shape | Example | What it returns |
|---|---|---|
| Product detail | `https://www.aldi.com.au/product/<slug>-<sku>` | That one product, full detail. |
| Category listing | `https://www.aldi.com.au/products/<path>/k/<categoryKey>` | That category's full result set. |
| Whole range | `https://www.aldi.com.au/products` | The entire catalogue (3184+ products), no category filter applied. |
| Special Buys (one date) | `https://www.aldi.com.au/special-buys/<YYYY-MM-DD>` | Everything on sale that day. Append `?theme=<Theme Name>` to narrow to one theme (e.g. `Pet Essentials`). |
| Special Buys (landing page) | `https://www.aldi.com.au/special-buys` | Auto-walks **every** on-sale date the landing page currently lists, one scope per date, no need to know the dates in advance. |
| Keyword search | `https://www.aldi.com.au/results?q=<term>` (or `/search?q=<term>`) | That keyword's full result set, same as Search mode. |
| Any listing + filter params | append `?brand-name=<Brand>&category-tree=<key>&theme=<Theme>&sort=<order>&page=<n>` to any category/Special-Buys/search link above | The filters the pasted URL's own query string carries are read straight off it and **override** the Brands/Themes/Sort by inputs for that URL; `page=<n>` starts the walk at that page instead of page 1; a page-started link never looks at the pages before it, so it skips Incremental mode's `EXPIRED` detection for that scope on that run (see below). |

Two ALDI pages are **not** product listings at all and are not supported: a pasted link to either logs a clear warning and is skipped rather than silently returning zero, `https://www.aldi.com.au/christmas` (a fixed, hand-curated showcase, not a filterable listing) and `https://www.aldi.com.au/aldi-prices` (a marketing/explainer page with no product data). Special Buys and weekly-specials ranges that only exist as ordinary category nodes (not date-scoped promotions) still work via the plain category-listing shape above, e.g. pasting the "Limited Time Only" category link.

Dates on the Special Buys pages are the site's own current promotion window and change weekly. The examples above (and in the URL field's prefill) show the *form* a link takes; paste today's actual link from aldi.com.au/special-buys for a live date, or just paste the bare landing page and let the actor find every current date for you.

#### Specials ranges and department control

Lower Prices, Super Savers and similar promo ranges report a large product count on ALDI's own site, but adding them to `categories` alongside your grocery aisles usually yields few *additional* rows: they cross-list products that already belong to a real department, and the actor's within-run dedupe keeps only the first copy it sees. To find what's currently discounted, filter on `onSale` / `wasPrice` on the record itself rather than on which range you queried it under. Use `excludeCategories` to drop a whole department from your output after the fact: it matches each record's own reported `categoryPath`, so it also removes products a promo range cross-listed from a department you never queried at all.

#### Resume and recurring updates

- **Resume from run/dataset** (`resumeFromRunId`) continues one specific interrupted crawl: paste the earlier run's id and this run skips every product id it already collected, so you don't pay twice. If this run itself is resumed by the Apify platform (a migration or Resurrect), that in-progress state always takes precedence and the pasted id is ignored.
- **Incremental mode** (`incrementalMode`) is for a search/category/URL set you run on a **schedule**. Each product is classified `NEW`, `UPDATED` (with `changedFields` naming exactly what changed), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED`, or `EXPIRED`. `REAPPEARED` only ever fires for a product a *previous* run had already tombstoned as `EXPIRED`; it is not a generic "back after a gap" signal. `EXPIRED` itself is only ever produced once a run has proved a **complete, uncapped scan** of every search term, category and URL it was given, with no `resumeFromRunId` in play; a Max-products cap, an unresolved category or URL, or a fetch failure all withhold `EXPIRED` detection for that run rather than risk a false positive, and the run log explains why. Every `UPDATED` row also carries `previousValues`, the value each changed field had last time, so a price change reads as "was 7.99, now 7.29" without keeping your own copy of the last run. If a field turns out to be noisy on your own run cadence, list it in `ignoreFieldsForChanges` so it no longer makes a product count as `UPDATED` (it is still returned in the output). With Incremental mode off, output is exactly as before.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each product, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | How to find products: `search` (keyword/category/specials) or `url` (paste ALDI links). |
| `searchTerms` | array | no default (prefill: `["milk"]`) | Keywords to search the catalogue (search mode); each term is paginated fully up to Max products. |
| `categories` | array | no default (empty) | Category names, URL slugs or numeric keys to list, including Special Buys, Lower Prices, Super Savers and any grocery aisle. |
| `specialsCategory` | string | `""` (no specials filter) | Add a promotional range directly: Special Buys, Lower Prices or Super Savers (search mode). |
| `excludeCategories` | array | no default (empty) | Department names to drop from the OUTPUT, matched against each product's own reported `categoryPath`. |
| `brands` | array | no default (empty) | Filter by brand name as shown on ALDI. |
| `themes` | array | no default (empty) | Filter by ALDI theme label. |
| `specialBuysDates` | array | no default (empty) | Special Buys on-sale dates to scrape (search mode), e.g. `2026-10-08`, `8 Oct` or `Wed 8 October`; `all` covers every date on ALDI's Special Buys page, `upcoming` the dates from today onward. Brands/Themes still apply. |
| `sortBy` | string | `relevance` | Result ordering, applied by ALDI itself before Max products is reached: `relevance`, `price_asc`, `price_desc`, `name` (A-Z) or `name_desc` (Z-A). |
| `onSaleOnly` | boolean | `false` | Only return products on a promotion: a Special Buy, a promotional range (Limited Time Only, Lower Prices, Super Savers) or a was-price. Filtered-out products are not returned and not charged. |
| `minPrice` | number | no default (empty) | Inclusive minimum price in AUD. Filtered-out products are not returned and not charged. |
| `maxPrice` | number | no default (empty) | Inclusive maximum price in AUD. Filtered-out products are not returned and not charged. |
| `urls` | array | no default (prefill: one example of every supported shape) | ALDI URLs to scrape (url mode): product, category, whole-range, Special Buys date/landing, or search URLs. A pasted URL's own filter query params override the Brands/Themes/Sort by inputs for that URL. See the URL-shape table above. |
| `fetchDetails` | boolean | `false` | Add full detail fields to every product; one extra request and one detail-enrichment charge per product. A pasted product URL always fetches full detail regardless of this toggle, but is only charged for it when this is on. |
| `maxItems` | integer | `20` | Maximum products to scrape across all terms/categories/URLs. `0` = unlimited. |
| `maxPages` | integer | no default (empty = no extra page cap) | Optional safety cap on result pages per term/category (60 products per page); the run still stops at Max products. |
| `includeCategoryTree` | boolean | `false` | Also save ALDI's full category tree (key, name, path, URL, parent, depth) to the key-value store record `CATEGORY_TREE`. Not added to the dataset, not charged. |
| `proxy` | object | no default (prefill: Apify Proxy, automatic) | Proxy used for requests. On a plan without Apify proxy access the run warns once and continues on a backup connection (see Plan Requirement). |
| `resumeFromRunId` | string | no default (empty) | Paste a previous run id from this actor to continue one interrupted crawl without duplicate pushes or charges. |
| `incrementalMode` | boolean | `false` | Remember this search's baseline across scheduled runs and return only NEW/UPDATED/REAPPEARED/EXPIRED products. |
| `stateKey` | string | no default (empty) | Manual key for Incremental mode's baseline; leave empty to derive one automatically from this run's search scope. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) every unchanged product every run (Incremental mode). |
| `emitExpired` | boolean | `false` | Also return (and bill) products no longer found after a complete scan (Incremental mode). |
| `ignoreFieldsForChanges` | array | no default (empty) | Incremental mode only: output field names that should NOT make a product count as `UPDATED`, even when they change (matches a field name at any nesting depth, e.g. inside a nested object). Measured empty by default: a live re-fetch of the same products found no rotating/volatile field on this actor's data. Ignored fields are still returned in the output. |
| `mcpConnectors` | array | no default (empty) | Optional: send a summary of each product to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | no default (empty) | Notion connector only: page under which product pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run; does not affect the dataset. |

#### Output views

Next to the default table, the dataset's **Products Overview** output carries every field in one row. Three narrower views give you the same rows with just the columns for one job: **Detail enrichment** (the fields `fetchDetails` adds), **Changes (incremental mode)** (`changeType`, `changedFields`, `previousValues`, `firstSeenAt`/`lastSeenAt` next to the product's name, price and link) and **Special Buys** (main image, on-sale date, theme, promo label and savings alongside price). Switch between them on the run's **Dataset** tab, or fetch one directly through the API with `?view=detail`, `?view=changes` or `?view=specialBuys`.

### Output Example

> Sample shape: values are illustrative placeholders, not from a live listing.

```json
{
  "id": "000000000011112222",
  "sku": "000000000011112222",
  "abstractSku": "000000000011112222A",
  "name": "Full Cream Milk 2L",
  "title": "SAMPLE BRAND Full Cream Milk 2L",
  "brand": "SAMPLE BRAND",
  "slug": "full-cream-milk-2l",
  "url": "https://www.aldi.com.au/product/full-cream-milk-2l-000000000011112222",
  "price": 7.29,
  "priceDisplay": "$7.29",
  "wasPrice": 8.99,
  "wasPriceDisplay": "$8.99",
  "savingsAmount": 1.7,
  "savingsPercent": 18.9,
  "savingsDisplay": "Save $1.70",
  "promoLabel": "Save $1.70",
  "onSale": true,
  "promotions": ["Lower Prices"],
  "unitPrice": 3.65,
  "unitPriceDisplay": "$3.65 per 1 L",
  "unitOfMeasure": "L",
  "unitMeasure": "1L",
  "perUnitPrice": null,
  "perUnitQuantity": null,
  "perUnitQuantityUnit": null,
  "sellingSize": "2 L",
  "currency": "AUD",
  "category": "Milk",
  "categoryPath": "Dairy, Eggs & Fridge > Milk",
  "department": "Dairy, Eggs & Fridge",
  "categories": [{ "id": "960000000", "name": "Dairy, Eggs & Fridge", "path": "dairy-eggs-fridge" }, { "id": "1111111160", "name": "Milk", "path": "dairy-eggs-fridge/milk" }],
  "isSpecialBuy": false,
  "notForSale": false,
  "images": ["https://cdn.aldi.com.au/products/full-cream-milk-2l.jpg"],
  "imagesCount": 1,
  "imageUrl": "https://cdn.aldi.com.au/products/full-cream-milk-2l.jpg",
  "categoryUrl": "https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160",
  "themes": [],
  "description": "Full product description appears here when Fetch product details is on.",
  "ingredients": "Full-cream milk.",
  "allergens": [],
  "allergenStatements": ["Contains: Milk"],
  "storageInstructions": "Keep refrigerated.",
  "countryOrigin": "Australia",
  "detailFetched": true,
  "position": 3,
  "input": "milk",
  "inputType": "searchTerm",
  "scrapedAt": "2026-10-06T01:02:03Z"
}
```

#### Extra outputs in the key-value store

These never go into the dataset and are never charged:

| Record | When | What it holds |
| --- | --- | --- |
| `OUTPUT` | every run | Run summary, plus one entry per search term, category, Special Buys date or URL with ALDI's reported total, how many new rows it gave, and `availableFilters`: the brands, themes and categories ALDI offers for that search (each with its product count) and its sort orders. Use those values directly in the Brands, Themes and Sort by inputs. |
| `SPECIAL_BUYS_CALENDAR` | runs that touch Special Buys (the landing-page URL, or the `all` / `upcoming` dates) | Every on-sale date ALDI currently lists, with its title (e.g. "Available from Wed 7th October"), its themes and its link. |
| `CATEGORY_TREE` | `includeCategoryTree` on | ALDI's full category tree (key, name, path, URL, parent, depth). |

### Plan Requirement

ALDI's product and detail data is served by the storefront's own JSON backend, so the prefilled connection (Apify Proxy, automatic) is enough for most searches, categories and URL runs on a paid plan. A residential connection is available under **Proxy & connection** if you want one for large or very frequent runs. Apify free plans include no Apify proxy access, so a free run says so in a boxed warning at the start and continues on a backup connection; if that connection cannot load the requested pages either, the run fails with a message saying so instead of returning an empty result. For residential access, upgrade your Apify plan or supply your own proxy URLs. The **Pricing** tab on the Store page shows the current per-event rates for this Pay-Per-Event actor.

### FAQ

#### How much does it cost?

This is a Pay-Per-Event actor: you pay for each product returned in the dataset, plus a detail-enrichment charge only for products returned while **Fetch product details** is on (a pasted product URL always fetches full detail, but is only charged for it under the same condition). A start event is charged once per run based on the memory used. The **Pricing** tab shows the current per-event rates. Use **Max products** to cap the cost of any run.

#### Is it legal to scrape ALDI Australia?

This actor collects only publicly available product and pricing data from aldi.com.au. You are responsible for how you use it: follow ALDI's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution.

#### Can I get only new, changed or expired products on a schedule?

Yes. Schedule the actor from the **Schedules** tab with **Incremental mode** on. Each run then returns only NEW and UPDATED products by default; UNCHANGED ones are suppressed and not billed. Turn on **Emit expired products** to also get a one-time EXPIRED row for anything that disappeared, but only once a run completes a full, uncapped scan of every search term, category and URL you gave it; a capped, partial or resumed run skips EXPIRED detection for that run and logs why.

#### Why did Lower Prices or Super Savers return fewer new products than ALDI's own count?

Those promotional ranges cross-list products that already belong to a real grocery aisle. If your run also scrapes that aisle, the actor's within-run dedupe keeps only the first copy of each product, so the promo range itself contributes few *additional* rows even though ALDI reports a large total for it. This is expected, not under-scraping: filter on `onSale` / `wasPrice` on the record itself to see what's currently discounted.

#### Why did my run fail instead of returning an empty dataset?

If no ALDI results page could be loaded at all, the run fails with a clear status message instead of silently returning nothing, so "blocked" is never confused with "no matching products". There are three: the proxy connection was refused for your account (your plan has no proxy access, or your own proxy URLs have wrong credentials); a free-plan run whose backup connection could not load the pages either; or any other connection problem, which usually clears if you run it again in a few minutes. By contrast, a search with no matching results completes normally with an empty dataset and a status message, and a URL list where every link is unrecognizable completes normally with an empty dataset and an explanatory log message, since neither is a connectivity problem.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push a summary of each product into Notion, Linear or Airtable.

### 🔗 Want more grocery data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🥦 <a href="https://apify.com/abotapi/coles-au-scraper"><b>Coles AU Scraper</b></a><br>Scrape Coles.com.au grocery products and customer reviews. Search by keyword, browse...</td><td>🥦 <a href="https://apify.com/abotapi/woolworths-au-scraper"><b>Woolworths AU Scraper</b></a><br>Scrape Woolworths Australia (woolworths.com.au) grocery products and reviews. Search by...</td></tr>
<tr><td>🥦 <a href="https://apify.com/abotapi/carrefour-es-scraper"><b>Carrefour Spain Scraper</b></a><br>Scrape Carrefour Spain (carrefour.es) products. Search by keyword, browse a category, or...</td><td>🥦 <a href="https://apify.com/abotapi/instacart-grocery-price-scraper"><b>Instacart Scraper</b></a><br>Scrape Instacart grocery catalogs with per-store prices. Run keywords across several...</td></tr>
<tr><td>🥦 <a href="https://apify.com/abotapi/flashfood-grocery-deals-scraper"><b>Flashfood Scraper</b></a><br>Scrape Flashfood discounted groceries and stores by location or store. Extract current...</td><td>🍷 <a href="https://apify.com/abotapi/danmurphys-scraper"><b>Dan Murphy’s Scraper</b></a><br>Scrape Dan Murphy’s products by category, keyword, or specials. Extract 45+ fields...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/aldi-com-au-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

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

How to find products. 'search' builds listings from search terms and/or categories with filters; 'url' scrapes ALDI product, category or search URLs you paste.

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

Keywords to search the ALDI catalogue, e.g. 'milk', 'coffee', 'air fryer'. Each term is paginated fully (up to Max products). Leave empty to use categories instead.

## `categories` (type: `array`):

Category names or URL slugs to list, e.g. 'Special Buys', 'Limited Time Only', 'Lower Prices', 'Super Savers', 'Dairy Eggs & Fridge', 'meat-seafood/beef'. Also accepts a raw numeric category key. Special Buys and weekly-specials ranges are categories here. Leave empty to use search terms instead.

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

Scrape a promotional range directly (search mode). ALDI Australia's promo ranges are Special Buys (Limited Time Only), Lower Prices and Super Savers. Adds the chosen range to the run alongside any search terms or categories. To scrape a specials listing in url mode instead, paste its ALDI listing URL. Leave as 'No specials filter' for a normal catalogue run.

## `specialBuysDates` (type: `array`):

Scrape the Special Buys that go on sale on these dates (search mode), e.g. '2026-10-08', '8 Oct' or 'Wed 8 October'. Enter 'all' for every date currently listed on ALDI's Special Buys page, or 'upcoming' for the dates from today onward. Each date is added to the run alongside any search terms or categories, and the Brands/Themes filters still apply. Leave empty to skip.

## `excludeCategories` (type: `array`):

Department names to drop from the OUTPUT, e.g. 'Liquor', 'Limited Time Only', 'Front of Store'. Matched against each product's OWN reported department (its categoryPath), not just against what you listed in Categories/Specials; it also catches products that promo ranges like Lower Prices, Super Savers or The People's Picks cross-list from an excluded department. Leave empty to keep everything.

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

Filter by brand name as shown on ALDI, e.g. 'EXPRESSI', 'URBAN EATS'. Leave empty for all brands. In url mode this is the fallback: a pasted URL's own 'brand-name' query param overrides it for that URL. Every run lists the brands ALDI offers for each search in the OUTPUT record (availableFilters).

## `themes` (type: `array`):

Filter by ALDI theme label, e.g. 'BBQ', 'Gardening', 'Pet Essentials', 'Christmas', 'Halloween', 'Liquor'. Leave empty for all. In url mode this is the fallback: a pasted URL's own 'theme' query param overrides it for that URL. Every run lists the themes ALDI offers for each search in the OUTPUT record (availableFilters), and Special Buys runs save the current themes per on-sale date in SPECIAL_BUYS_CALENDAR.

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

Result ordering, applied by ALDI itself before Max products is reached, so 'Price: low to high' with Max products 20 returns ALDI's 20 cheapest matches. In url mode this is the fallback: a pasted URL's own 'sort' query param overrides it for that URL.

## `onSaleOnly` (type: `boolean`):

Only return products that are on a promotion: a Special Buy (has an on-sale date), on a promotional range (Limited Time Only, Lower Prices or Super Savers), or showing a was-price. Applies in search mode and url mode. Products filtered out are not returned and not charged.

## `minPrice` (type: `number`):

Only return products priced at or above this amount, in Australian dollars. Applies in search mode and url mode. Products filtered out are not returned and not charged. Leave empty for no minimum.

## `maxPrice` (type: `number`):

Only return products priced at or below this amount, in Australian dollars. Applies in search mode and url mode. Products filtered out are not returned and not charged. Leave empty for no maximum.

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

ALDI URLs to scrape (url mode): product pages, category listings (incl. the bare /products page for the whole range), Special Buys on-sale-date pages (incl. the bare /special-buys landing page, which covers every date it currently lists), and search-result pages. Product URLs return a single detail record; every other shape returns its full result set (up to Max products). A pasted URL's own filter query params (brand-name, category-tree, theme, sort, page) are honoured straight off that URL and override the Brands/Themes/Sort by inputs above for that URL only; see the README for the full list of supported shapes.

## `fetchDetails` (type: `boolean`):

Add full product-detail fields to every result: long description, ingredients, allergens, nutritional claims, usage and storage instructions, country of origin and the complete image gallery. Adds one request per product.

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

Maximum number of products to scrape across all terms/categories/URLs. Set 0 for unlimited.

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

Optional safety cap on the number of result pages per search term or category (60 products per page). Leave empty for no page limit; the run stops at Max products.

## `includeCategoryTree` (type: `boolean`):

Also save ALDI's full category tree (every department and aisle with its key, name, path, URL, parent and depth) to the run's key-value store as the record CATEGORY_TREE. Not added to the dataset and not charged. Off by default.

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

Proxy used for requests. Any Apify proxy works; the default is fine for most runs. Runs on any Apify plan.

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

Paste a previous run id from THIS actor to continue one interrupted crawl: this run skips every product id that run already collected, so a capped or aborted crawl can pick up where it left off without duplicate pushes/charges. A separate feature from Incremental mode below; see its description for when to use each. Leave empty for a normal run.

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

For a search/category/URL set you run on a schedule (daily/weekly): the actor remembers its own baseline for this exact search and returns only NEW / UPDATED / REAPPEARED / EXPIRED products next time, instead of the full catalogue again. Unlike Resume from run/dataset above (which continues ONE specific interrupted run), this remembers the search itself across separate scheduled runs. Off by default so a normal run's output is unchanged.

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

Only used with Incremental mode. Leave empty to key the baseline automatically off this run's search terms/categories/specials/brands/themes/sort/URLs/fetchDetails (two different searches never share state). Set your own text here to deliberately share one baseline across otherwise-different inputs, or to isolate multiple monitoring campaigns that would otherwise hash to the same key.

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

Only used with Incremental mode. By default an UNCHANGED product (identical to last run) is NOT returned and NOT billed. Turn this on to also return every unchanged product every run; this returns and bills every tracked product, not just what changed.

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

Only used with Incremental mode. When on, a product tracked by a previous run that no longer appears after a COMPLETE scan of this search is returned once with changeType EXPIRED, then dropped from the baseline. Skipped (with a log message) on a Max products-capped, resumed, or otherwise partial run, since an incomplete scan cannot tell 'gone' apart from 'not reached yet'. This returns and bills the expired rows in addition to NEW/UPDATED ones.

## `ignoreFieldsForChanges` (type: `array`):

Output field names that should NOT make a product count as UPDATED, even when they changed. Measured empty by default on this actor (a live re-fetch of the same products found no rotating/volatile field), so nothing is ignored unless you add one here. Matches a field name at any nesting depth (for example inside nutritionalInformation). Ignored fields are still returned in the output.

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

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify, Settings, Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON; the complete record stays in the dataset. Leave empty to skip. 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",
  "searchTerms": [
    "milk"
  ],
  "specialsCategory": "",
  "sortBy": "relevance",
  "onSaleOnly": false,
  "urls": [
    "https://www.aldi.com.au/product/expressi-milk-frother-000000000000545135",
    "https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160",
    "https://www.aldi.com.au/products",
    "https://www.aldi.com.au/special-buys/2026-10-07?theme=Pet+Essentials",
    "https://www.aldi.com.au/special-buys",
    "https://www.aldi.com.au/results?q=milk",
    "https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160?brand-name=FARMDALE&sort=price_asc"
  ],
  "fetchDetails": false,
  "maxItems": 20,
  "includeCategoryTree": false,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `specialBuys` (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",
    "searchTerms": [
        "milk"
    ],
    "urls": [
        "https://www.aldi.com.au/product/expressi-milk-frother-000000000000545135",
        "https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160",
        "https://www.aldi.com.au/products",
        "https://www.aldi.com.au/special-buys/2026-10-07?theme=Pet+Essentials",
        "https://www.aldi.com.au/special-buys",
        "https://www.aldi.com.au/results?q=milk",
        "https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160?brand-name=FARMDALE&sort=price_asc"
    ],
    "proxy": {
        "useApifyProxy": true
    },
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/aldi-com-au-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",
    "searchTerms": ["milk"],
    "urls": [
        "https://www.aldi.com.au/product/expressi-milk-frother-000000000000545135",
        "https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160",
        "https://www.aldi.com.au/products",
        "https://www.aldi.com.au/special-buys/2026-10-07?theme=Pet+Essentials",
        "https://www.aldi.com.au/special-buys",
        "https://www.aldi.com.au/results?q=milk",
        "https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160?brand-name=FARMDALE&sort=price_asc",
    ],
    "proxy": { "useApifyProxy": True },
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/aldi-com-au-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 '{
  "mode": "search",
  "searchTerms": [
    "milk"
  ],
  "urls": [
    "https://www.aldi.com.au/product/expressi-milk-frother-000000000000545135",
    "https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160",
    "https://www.aldi.com.au/products",
    "https://www.aldi.com.au/special-buys/2026-10-07?theme=Pet+Essentials",
    "https://www.aldi.com.au/special-buys",
    "https://www.aldi.com.au/results?q=milk",
    "https://www.aldi.com.au/products/dairy-eggs-fridge/milk/k/1111111160?brand-name=FARMDALE&sort=price_asc"
  ],
  "proxy": {
    "useApifyProxy": true
  },
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/aldi-com-au-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/aldi-com-au-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/rfbg3YFyGXtuzsSwt/builds/mlCPHgwim6P4eMVAy/openapi.json
