# Dan Murphy’s Scraper - Prices, Stock, Ratings & Specials (`abotapi/danmurphys-scraper`) Actor

Scrape Dan Murphy’s products by category, keyword, or specials. Extract 45+ fields, including regular, sale and member prices, case and unit pricing, stock, ratings, reviews, country, variety, and ABV. Incremental mode tracks changes.

- **URL**: https://apify.com/abotapi/danmurphys-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Features:** Limited permissions
- **Stats:** 3 total users, 0 monthly users, 86.2% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 product listings

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

## Dan Murphy's Scraper: Products, Prices & Member Offers

Dan Murphy's Scraper turns danmurphys.com.au, Australia's largest liquor retailer, into structured product data. Get regular, sale and member-offer prices, case and per-unit pricing, ratings, review counts, stock levels and product attributes (country, style, ABV, standard drinks) across the departments the site's data service currently answers. Browse by department, search by keyword, filter by brand, country, region, style, price or specials, or paste category and product links directly, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **Two ways to find products.** Browse a department (with sub-department, brand, country, region and style filters), or paste category, style and product links directly. No need to build URLs by hand. Beer, spirits, whisky, accessories and gifts answer today; the site's data service rejects the wine, premix and some other routes right now, and the run log says so instead of returning an empty list.
- **Every price that applies, at once.** Regular price, sale price, member-only offers, case pricing and per-unit pricing, so you always know which price a shopper actually pays.
- **45+ fields per product.** Ratings, review counts, stock levels, country of origin, variety/style, ABV, standard drinks, awards, closures and more.
- **Specials and member-offer filters in both modes.** Narrow any department, sub-department or pasted category link to only current offers or member deals.
- **Built for scheduled price-watch runs.** Incremental mode classifies every product NEW, UPDATED, UNCHANGED, REAPPEARED or EXPIRED between runs, so a recurring schedule reports only what changed.
- **Optional detail-page enrichment.** Switch on rich descriptions, tasting notes and recommended products only when you need them, charged only for the products it actually fetches.
- **A refused or mistyped run fails loudly instead of returning an empty dataset.** A category the site rejects, a sub-department or brand that matches nothing (the log lists the valid names) and a connection failure each say what happened, so "nothing matched" is never confused with "couldn't be read".

### Use Cases

- **Price monitoring and comparison tools:** track regular, sale and member-offer prices across departments to power price-history or comparison sites.
- **Liquor retailers and distributors:** watch a competitor's specials, stock levels and new listings in your category.
- **Wine and spirits content creators:** build rating- and review-based recommendation lists by variety, style, country or ABV.
- **Market researchers and analysts:** track assortment changes, award-winning products and how often member offers appear over time.
- **Personal shopping tools and alerts:** get notified when a tracked product or department goes on special.

### Data You Get

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

| Field | Example |
| --- | --- |
| `stockcode` | `"812345"` |
| `name` | `"Sample Brewing Co Pale Ale 6x355mL Cans"` |
| `brand` | `"Sample Brewing Co"` |
| `department` | `"beer"` |
| `url` | Dan Murphy's link to the product |
| `price` / `wasPrice` | `45.99` / `52.99` (AUD) |
| `isOnSpecial` / `isOnOffer` | `true` / `true` |
| `isMemberOffer` / `memberOfferLabel` | `true` / `"Member Price"` |
| `casePrice` / `singlePrice` | `179.99` / `52.99` |
| `inAnySixPrice` | `42.99` |
| `rating` / `reviewCount` | `4.3` / `87` |
| `country` | `"Australia"` |
| `style` | `"Pale Ale"` (beer style, wine variety or spirit type) |
| `size` / `packageSize` | `"355mL"` / `"6 pack"` |
| `abv` / `standardDrinks` | `"4.5%"` / `"9.8"` |
| `stockQty` / `purchasable` | `240` / `true` |
| `deliveryOnly` | `false` |
| `award` | `"Gold Medal"` |
| `badges` | `["Top seller", "Queensland, Australia"]` (the badges the site shows on the product) |
| `categoryUrl` | the link this product was collected from (your pasted link, or the site link of the department / sub-department / keyword) |
| `imageUrl` | product image link |
| `richDescription` / `tastingNotes` | long-form description / tasting notes (when `fetchDetails` is on) |
| `recommendedStockcodes` | related product stock codes (when `fetchDetails` is on) |
| `changeType` / `changedFields` | `"UPDATED"` / `["price"]` (incremental mode) |
| `firstSeenAt` / `lastSeenAt` | ISO timestamps |

Also carried when present: `sash` (the badges as raw text, kept for stable change detection; use `badges`), `description`, `promotionType`, `packMessage`, `casePackMessage`, `unit`, `closure`, `foodMatch`, `isNew`, `imageLarge`, and `detailFetched` (true only on a product whose detail payload was actually fetched this run).

### How to Use

1. Pick a **mode**: `search` (department, sub-department, keyword, brand) or `url` (paste category or product links).
2. Fill in the fields for that mode, and switch on `specialsOnly` / `memberOffersOnly` if you only want current or member offers.
3. Set **Max products per scope** to control run size and cost (it applies to each scope separately, see the tip below), then click **Start**.
4. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Browse a department with a specials filter:**

```json
{
  "mode": "search",
  "department": "beer",
  "subDepartment": "craft beer",
  "specialsOnly": true,
  "maxProducts": 50
}
```

**Keyword search plus a department (two scopes in one run):**

```json
{
  "mode": "search",
  "department": "beer",
  "searchTerm": "heineken zero",
  "maxProducts": 100
}
```

The keyword runs as its own site-wide search next to the department scope; it is not narrowed to the department, and the specials, member-offer and brand filters do not apply to it. The price and the country / region / style / variety filters do. When the site answers a keyword with a category, style or brand page instead of products (for example `pale ale`, `vodka` or `guinness`), that page is read instead; a keyword whose curated page returns no products is reported in the log, never an empty success.

**Paste a category link, member offers only:**

```json
{
  "mode": "url",
  "urls": ["https://www.danmurphys.com.au/whisky/scotch-whisky"],
  "memberOffersOnly": true,
  "maxProducts": 0
}
```

**Recurring price-watch run (scheduled, incremental):**

```json
{
  "mode": "search",
  "department": "spirits",
  "incrementalMode": true,
  "emitExpired": true,
  "maxProducts": 0
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/danmurphys-scraper").call(run_input={"mode": "search", "department": "beer", "maxProducts": 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/danmurphys-scraper').call({ mode: 'search', department: 'beer', maxProducts: 50 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

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

#### Caps and run size

**Max products per scope** is counted per scope, not across the whole run. Each scope (the keyword search, the department, and every pasted category link) scans up to that many products on its own, so a run with a keyword and a department can return up to twice the cap, and three pasted category links up to three times it. Pasted product links are not counted against the cap at all. To bound a run's total size, divide your budget by the number of scopes. **Max pages per scope** (24 products per page) also applies to each scope separately.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run id and products it already collected are skipped, so you don't pay twice. It cannot be combined with incremental mode: the two solve different problems (one continues a single run, the other tracks a recurring scope), and the run refuses to start if both are set.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same search. Each product is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED` or `EXPIRED` (only after a run that scanned its whole tracked scope). `stateKey` names or shares the stored baseline; leave it empty to auto-key on mode, department and filters.

**What counts as a change.** Every output field is compared (price, was price, special and member-offer flags, case/single/any-six prices, stock quantity, purchasable and delivery flags, name, description, ratings, badges, images). `scrapedAt`, `categoryUrl` and the detail-enrichment flags are never compared. Use **Ignore fields when detecting changes (incremental mode only)** (`ignoreFieldsForChanges`) to leave more fields out; the fields stay in the output but never create an UPDATED row or appear in `changedFields`. Dotted paths such as `variations.image` drop that key from every array element.

**Price and stock monitoring (all optional, off by default).** Set `changeScope` to `priceAndStock` so a product is UPDATED only when a price, special flag or stock field moves. Description, image and label changes then never create a row. Turn on `compactUpdates` for slim UPDATED and EXPIRED rows (ids, url, name, current price and stock, and `priceChanges` with old and new values); compact UPDATED rows skip detail enrichment, so they carry no detail-enrichment charge. `expireAfterMissedScans` (default 1) marks a product EXPIRED only after it was missing from that many consecutive complete scans. EXPIRED means the product dropped out of the scanned listing, which a delisting and a temporary drop both cause; read `stockQty` and `purchasable` for availability. Changing the scope or ignore list under an automatic state key starts a fresh baseline; set a `stateKey` to keep one.

**Tip: use `maxProducts: 0` (or a cap larger than the scope) for incremental monitoring.** The cap counts products scanned, including unchanged ones, and every run starts again from the first page. A capped incremental run therefore re-checks the same first products every time: price changes, new listings and removals further down the list are never seen, and the EXPIRED pass is skipped because the scan never finishes. Once those first products stop changing, a capped run can return 0 rows and log "No products matched" even though it scanned its full allowance; that means "nothing changed in the part that was scanned", not "nothing changed". The default cap is 20, so raise it before scheduling.

**Tip:** `emitExpired` batches respect `maxProducts`. When `maxProducts` is above 0, the EXPIRED rows pushed (and billed) in a run are limited to `maxProducts` minus every product row already pushed in that run, pasted product links included even though those links do not count against the per-scope cap. EXPIRED rows therefore never add to a run that has already reached `maxProducts`. Because `maxProducts` is applied per scope, a run with several scopes can already have pushed more than `maxProducts`; in that case no EXPIRED rows are emitted in that run and the missing products stay remembered for a later run. Any further missing products are left remembered, not expired, and are not billed in this run. With `maxProducts: 0` the EXPIRED batch is not limited, so every product missing from a complete scan is pushed as EXPIRED, however many that turns out to be. A capped run rarely finishes its scan, and the EXPIRED pass only runs after a complete scan. If a tracked scope shrinks a lot between scheduled runs, start a fresh `stateKey` (or narrow the scope) and check the run log's change-count summary before widening a schedule.

#### Supported URL shapes

| Shape | Example | Behaviour |
| --- | --- | --- |
| Sub-department | `https://www.danmurphys.com.au/beer/craft-beer` | Walks the category. |
| Sub-department, multi-word | `https://www.danmurphys.com.au/whisky/scotch-whisky` | Walks the category. |
| Department only | `https://www.danmurphys.com.au/spirits` | Walks the whole department. |
| Style page | `https://www.danmurphys.com.au/beer/beerstyle-pale-ale` | Walks the department with that style filter applied. |
| Product page | `https://www.danmurphys.com.au/product/DM_560632/balter-xpa-cans-375ml` | One product row with the same price, offer, stock and attribute fields as a listing row. |
| Search link | `https://www.danmurphys.com.au/search?searchTerm=pale%20ale` | Runs a keyword scope; when the site answers the term with a category or style page, that page is walked. |
| `?page=N` suffix | `.../beer/craft-beer?page=2` | The suffix is not used (warning logged); the walk starts at page 1. |

Unrecognised links are skipped with a warning. A link the site's data service rejects (`Category does not exist`, today including the wine departments) or a product code that does not exist is named in the log, and a run in which every scope was rejected fails with that message instead of finishing empty. In an incremental run, a rejected or unreadable scope also switches the EXPIRED pass off, so a dead link can never expire the products of a live one.

### 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, or a compatible MCP target).
2. Select it in the **MCP connectors** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where products 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` | `search` (department / keyword) or `url` (paste category or product links). |
| `department` | string | `beer` | Department slug. `beer`, `spirits`, `whisky`, `accessories` and `gifts` answer today; the wine, premix, other-drinks, zero-alcohol and food routes are currently rejected by the site's data service (reported in the log). |
| `subDepartment` | string | (none) | Sub-department name, case-insensitive (`Craft Beer`, `craft-beer`, `Scotch Whisky`, `Vodka`). A name that matches nothing stops that scope with an error listing the valid names. Leave empty for the whole department. |
| `searchTerm` | string | (none) | Free-text search. If given, a keyword scope runs in addition to the department scope (site-wide; department, brand and offer filters do not apply to it). |
| `brand` | string | (none) | Brand name or slug, case-insensitive (`Stone & Wood`, `stone-and-wood`). A brand that matches nothing stops that scope with an error listing the valid brands; it is never silently ignored. Search mode only. |
| `urls` | array | five example links | Category routes and/or product links. Multi-URL supported; pagination walks forward automatically. In url mode with this field untouched, five examples are scraped: a sub-department, a multi-word sub-department, a whole department, a department /all link and a product link. |
| `specialsOnly` | boolean | `false` | Only products with a current offer. Applies to the department scope and every URL-mode category (not to the keyword scope or pasted product links). |
| `memberOffersOnly` | boolean | `false` | Only products carrying a member (My Dan Murphy's) offer. Applies to the department scope and every URL-mode category (not to the keyword scope or pasted product links). |
| `minPrice` | number | (none) | Lowest price in AUD, using the site's own price filter. Applies to the keyword scope, the department scope and every URL-mode category. |
| `maxPrice` | number | (none) | Highest price in AUD; either bound may be left empty. Same scope as `minPrice`. |
| `sortBy` | string | `Relevance` | Site-native sort: `Relevance`, `Name` (A-Z), `PriceAsc` (low to high) or `PriceDesc` (high to low). With a small `maxProducts` this picks the cheapest or dearest items. |
| `country` | string | (none) | Country of origin facet, case-insensitive; comma-separated for several. Applies to the department scope, the keyword scope and every category URL; no match stops that scope with an error listing the valid values. |
| `region` | string | (none) | Region facet (for example `Geelong`), same rules as `country`. |
| `style` | string | (none) | Style facet of the department (beer style such as `IPA`, whisky style ...), same rules as `country`. |
| `variety` | string | (none) | The site's variety facet (the department's types such as `Craft Beer`), same rules as `country`. |
| `fetchDetails` | boolean | `false` | Fetch each pushed product's detail payload (rich description, tasting notes, recommendations). Adds a per-product surcharge. |
| `maxPages` | integer | `0` | Cap on pages walked per scope. `0` walks until the department is exhausted. |
| `maxProducts` | integer | `20` | Cap on products scanned, applied to each scope separately (pasted product links are not counted). `0` = unlimited. Use `0` for incremental monitoring. |
| `proxyConfiguration` | object | Apify Proxy (datacenter) | Proxy and connection settings. The default Apify datacenter proxy works; Apify Residential with country AU is optional. |
| `resumeFromRunId` | string | (none) | Continue one interrupted run. Cannot be combined with incremental mode. |
| `incrementalMode` | boolean | `false` | For scheduled monitoring: classify every product NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED against the previous run of the same search. |
| `stateKey` | string | (none) | Manual key for the incremental baseline. Leave empty to auto-key on mode, department and filters (including price, sort and the facet filters when set). |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return (and bill) products classified UNCHANGED. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return (and bill) products that vanished since the last complete run. |
| `changeScope` | string | `all` | Incremental mode only. `priceAndStock` marks a product UPDATED only when price or stock fields move. |
| `compactUpdates` | boolean | `false` | Incremental mode only. Slim UPDATED and EXPIRED rows with `priceChanges`. |
| `expireAfterMissedScans` | integer | `1` | Incremental mode only. Consecutive complete scans a product must be missing before it is EXPIRED. |
| `ignoreFieldsForChanges` | array | (none) | Incremental mode only. Fields (dotted paths allowed) left out of change detection. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each product to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which products are created. |
| `maxNotifyListings` | integer | `0` | Cap on items exported to connectors per run. `0` exports everything pushed. |

#### Output views

The dataset has three views in the Console: **Overview** (prices, offers, ratings, stock and key attributes), **Changes (incremental)** (change type, changed fields, price and stock) and **Product detail (fetchDetails)** (descriptions, tasting notes, awards, recommendations).

### Output Example

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

```json
{
  "stockcode": "812345",
  "name": "Sample Brewing Co Pale Ale 6x355mL Cans",
  "url": "https://www.danmurphys.com.au/product/812345/sample-brewing-co-pale-ale-6x355ml-cans",
  "description": "Sample Brewing Co Pale Ale 6x355mL Cans",
  "brand": "Sample Brewing Co",
  "department": "beer",
  "categoryUrl": "https://www.danmurphys.com.au/beer",
  "badges": ["Top seller"],
  "price": 45.99,
  "wasPrice": 52.99,
  "isOnSpecial": true,
  "isOnOffer": true,
  "isMemberOffer": true,
  "memberOfferLabel": "Member Price",
  "promotionType": "member offer",
  "casePrice": 179.99,
  "singlePrice": 52.99,
  "inAnySixPrice": 42.99,
  "unit": "Each",
  "packageSize": "6 pack",
  "abv": "4.5%",
  "standardDrinks": "9.8",
  "country": "Australia",
  "style": "Pale Ale",
  "size": "355mL",
  "rating": 4.3,
  "reviewCount": 87,
  "stockQty": 240,
  "purchasable": true,
  "imageUrl": "https://cdn.danmurphys.com.au/images/812345-medium.jpg",
  "scrapedAt": "2026-09-30T04:00:00Z",
  "changeType": "UPDATED",
  "changedFields": ["price", "stockQty"],
  "firstSeenAt": "2026-08-01T04:00:00Z",
  "lastSeenAt": "2026-09-30T04:00:00Z"
}
```

### Plan Requirement

The default Apify datacenter proxy works for this site, so no residential proxy is required and any plan that includes Apify Proxy can run it. The site's pages are an empty shell and the product data comes from its own backend, which the default connection reaches. Apify Residential with country AU is optional. Apify free plans do not include proxy access at all: on such a plan the run continues on a backup connection where possible and may still return nothing. Switch the proxy group under **Connection**, or supply your own proxy URLs.

### FAQ

#### How much does it cost?

Two billed events, both shown with their current rates on the **Pricing** tab. **Product listing** is charged once per product row returned (including EXPIRED and UNCHANGED rows only when you turn on `emitExpired` / `emitUnchanged`). **Detail-page enrichment** is charged once per pushed product whose detail payload was actually fetched, and only when `fetchDetails` is on. Suppressed unchanged rows and products that fail to load are not charged. Use **Max products per scope** to cap cost; it applies to each scope separately, so a run with a keyword plus a department, or several pasted category links, can return a multiple of it.

#### Is it legal to scrape Dan Murphy's?

This actor collects only publicly available product data. You are responsible for how you use it: follow Dan Murphy's terms and the laws that apply to you, including any age-verification and liquor-advertising rules in your jurisdiction, and get legal advice if you plan commercial redistribution. Dan Murphy's requires visitors to be of legal purchasing age; make sure your use of this data respects that.

#### Can I get only new or changed prices on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated, reappeared or expired products, and unchanged ones are not billed unless you turn on **Emit unchanged products**. Set **Max products per scope** to `0` for this: a capped run re-checks only the same first products every time and never detects expired ones.

#### Why does the "wine" department not work?

`wine` on its own is not a department, and the site's data service currently rejects the wine routes too (`red-wine`, `white-wine`, `champagne-sparkling`, `fortified-wine` and the premix, other-drinks, zero-alcohol and food routes answer "Category does not exist"; checked live on 2026-10-28). The run names the rejected route in the log, and a run where nothing else was requested fails with that message instead of finishing empty. Beer, spirits, whisky, accessories and gifts answer. The wine values stay selectable so they work again as soon as the site re-enables them.

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

If nothing can be read, the run stops with a clear message so "no products matched" is never confused with "nothing could be read". You will see one of these messages: the proxy connection was refused for your account (a plan with proxy access is required, or check your own proxy URLs); no results because residential proxies are unavailable and the backup connection could not load the pages; "Could not load any results page (the requests failed)", a connection problem to retry later; or "Nothing was collected: ...", which names the category, sub-department, brand, filter value or product code the site does not recognise (with the valid names where the site offers a list).

#### Can emitting expired products push more rows than my Max products setting?

No: when **Max products per scope** is above 0, EXPIRED rows never add to a run that has already reached that number. With **Emit expired products** on, the EXPIRED rows pushed (and billed) in a run are capped at **Max products per scope** minus every product row already pushed in that run, pasted product links included. Because the setting is applied per scope, a run with several scopes may already have pushed more rows than that number, and then no EXPIRED rows are emitted in that run; products beyond the cap stay remembered and are neither emitted nor billed in this run. With **Max products per scope** set to `0` (unlimited) the EXPIRED batch is not capped: every product missing from a complete scan is pushed as EXPIRED, so a scope that shrank a lot since the last complete run can bill many EXPIRED rows. Start a fresh **State key** for scopes that can shrink sharply, or check the run log's change-count summary before widening a schedule.

#### 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 results into Notion or another compatible connector.

### 🔗 Want more wine and drinks data?

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

<table>
<tr><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><td>🍷 <a href="https://apify.com/abotapi/untappd-scraper"><b>Untappd Beer, Brewery and Check-in Scraper</b></a><br>Scrape Untappd beers, breweries, venues and check-ins by keyword, brewery, Top Rated...</td></tr>
<tr><td>🍷 <a href="https://apify.com/abotapi/wine-searcher-scraper"><b>Wine-Searcher Scraper</b></a><br>Look up wines on wine-searcher.com by name, URL, or LWIN code. Returns 30+ fields...</td><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></tr>
<tr><td>🍷 <a href="https://apify.com/abotapi/vivino-wine-data-scraper"><b>Vivino Wine Scraper</b></a><br>Scrape Vivino.com for wine ratings, prices, taste profiles, food pairings, grapes, and...</td><td>🥦 <a href="https://apify.com/abotapi/aldi-com-au-scraper"><b>ALDI AU Scraper</b></a><br>Scrape ALDI Australia (aldi.com.au) products: name, brand, price, was-price, savings...</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/danmurphys-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`):

Search mode walks a department (optionally a sub-department, a keyword, a brand, specials-only). URL mode walks any category route or product link you paste — multiple allowed, pagination walks forward automatically.

## `department` (type: `string`):

Department route. Beer, spirits, whisky, accessories and gifts answer today; the site's backend currently rejects several others (the wine, premix, other-drinks, zero-alcohol and food routes answer 'Category does not exist'). A rejected department is reported in the run log, and a run where every scope was rejected fails with that message instead of finishing empty. Keep the enum values: the site may re-enable them.

## `subDepartment` (type: `string`):

Sub-department name, matched case-insensitively against the department's own list (e.g. 'Craft Beer' or 'craft-beer' for /beer/craft-beer, 'Scotch Whisky' for /whisky/scotch-whisky, 'Vodka' for /spirits/vodka). A name that matches nothing stops that scope with an error listing the valid values. Leave empty for the whole department.

## `searchTerm` (type: `string`):

Free-text search. If given, a keyword scope runs in addition to the department scope.

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

Brand name or slug, matched case-insensitively against the department's brand list ('Stone & Wood' or 'stone-and-wood', '4 Pines'). A brand that matches nothing stops that scope with an error listing the valid brands; it is never silently ignored. Search mode only.

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

Category routes (/beer/craft-beer, /whisky/scotch-whisky, /spirits) and/or product links (/product/{code}/{name}). Multi-URL supported. Pagination starts at the pasted URL's first page and walks forward. The specials, member-offers, price and facet filters in the next section apply to category URLs too. Search links (/search?searchTerm=...) run a keyword scope; if the site answers the term with a category or style page, that page is walked instead. A link the site rejects is reported in the run log. A ?page=N suffix is not used (the walk starts at page 1). Left untouched in url mode, the five example links are scraped (a sub-department, a multi-word sub-department, a department, a department /all link and a product link).

## `specialsOnly` (type: `boolean`):

Only products with a current offer (the site's currentoffer facet). Applies to the department scope in search mode and to every category URL in url mode (not to the keyword search or pasted product links).

## `memberOffersOnly` (type: `boolean`):

Only products carrying a member (My Dan Murphy's) offer. Applies to the department scope in search mode and to every category URL in url mode (not to the keyword search or pasted product links).

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

Only products priced at or above this amount (the site's native price filter). Leave empty for no lower bound. Applies to search mode and to every URL in url mode.

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

Only products priced at or below this amount. Leave empty for no upper bound.

## `country` (type: `string`):

Country of origin, matched case-insensitively against the site's own country facet (e.g. 'Australia'). Comma-separated for several. Applies to the department scope, the keyword scope and every category URL.

## `region` (type: `string`):

Region facet, matched case-insensitively against what the site offers for that department (e.g. 'Geelong'). Comma-separated for several.

## `style` (type: `string`):

Style facet of the department (beer style such as 'IPA' or 'Pale Ale', whisky style, ...), matched case-insensitively. Comma-separated for several.

## `variety` (type: `string`):

The site's variety facet, which lists the department's types (e.g. 'Craft Beer', 'Scotch Whisky'). Same list as the sub-department, applied as a filter. Comma-separated for several.

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

Native site sort applied to every scope. With a small Max products this selects the cheapest / most expensive items.

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

Fetch each pushed product's detail payload (tasting/rich description, recommendations). Adds a per-product surcharge — see pricing. Listing tiles already carry prices, member offers, ratings, stock and attributes without this.

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

Cap on pages walked per scope. 0 walks until the department is exhausted.

## `maxProducts` (type: `integer`):

Cap on products scanned, applied to each scope separately (the keyword search, the department and every pasted category link each get their own allowance; pasted product links are not counted). Default 20. 0 = unlimited (a full department can be tens of thousands of products).

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

Apify proxy. The default Apify datacenter proxy works for this site; if you see blocks, switch to Apify Residential with country AU. Apify free plans do not include proxy access: on such a plan the run continues on a backup connection where possible and may still return nothing, so supply your own proxy URLs or use a plan with proxy access.

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

Continue ONE interrupted run: products already collected by that run are skipped. Cannot be combined with incremental mode.

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

For scheduled monitoring: the run remembers the previous run of the SAME search and classifies every product NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED. Compared: every output field (price, was price, special and member-offer flags, case/single/any-six prices, stock quantity, purchasable/delivery flags, name, description, ratings, images...). Never compared: scrapedAt, categoryUrl (the link a row was collected from) and the detail-enrichment flags, plus anything listed in 'Ignore fields when detecting changes'. Defaults off — no existing user's runs change until they opt in.

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

Manual key for the incremental baseline. Leave empty to auto-key on mode + department + filters, so two different filter setups never share a baseline.

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

Incremental mode only. Also return (and bill) products classified UNCHANGED.

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

Incremental mode only. Also return (and bill) products that vanished since the last complete run. When Max products is above 0 the EXPIRED rows are limited to Max products minus the rows already pushed in the run.

## `changeScope` (type: `string`):

Incremental mode only. 'all' compares every field except the volatile ones. 'priceAndStock' marks a product UPDATED only when its price, was-price, special/member-offer flags, case/single/any-six prices, stock quantity or purchasable/delivery flags move; descriptions, images and labels never create an UPDATED row. Changing this under an auto state key starts a fresh baseline; use a State key to keep one.

## `compactUpdates` (type: `boolean`):

Incremental mode only. UPDATED and EXPIRED rows carry only ids, url, name, current price/stock and priceChanges {field: {old, new}} instead of the full record. NEW rows stay full. Compact UPDATED rows skip detail enrichment, so they carry no detail-enrichment charge.

## `expireAfterMissedScans` (type: `integer`):

Incremental mode only, with 'Emit expired products'. A product is marked EXPIRED only after it was missing from this many consecutive complete scans (1 = the first). EXPIRED means the product dropped out of the scanned listing; without a detail check a listing drop and a delisting look the same, so use the stock fields (stockQty, purchasable) for availability.

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

Incremental mode only. Field names to leave out when deciding whether a product changed (kept in the output, excluded from the comparison and from changedFields). Dotted paths such as variations.image drop that key from every array element. scrapedAt, categoryUrl and the detail-enrichment flags are always ignored.

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

Connector ids (authorized under Apify → Settings → Integrations) to push results into — Notion and compatible MCP targets. Optional.

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

For a Notion MCP connector: the parent page the results are written under. Optional.

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

Cap on items exported to connectors per run. 0 exports everything pushed.

## Actor input object example

```json
{
  "mode": "search",
  "department": "beer",
  "urls": [
    "https://www.danmurphys.com.au/beer/craft-beer",
    "https://www.danmurphys.com.au/whisky/scotch-whisky",
    "https://www.danmurphys.com.au/spirits",
    "https://www.danmurphys.com.au/whisky/all",
    "https://www.danmurphys.com.au/product/DM_560632/balter-xpa-cans-375ml"
  ],
  "specialsOnly": false,
  "memberOffersOnly": false,
  "sortBy": "Relevance",
  "fetchDetails": false,
  "maxPages": 0,
  "maxProducts": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "changeScope": "all",
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 0
}
```

# Actor output Schema

## `products` (type: `string`):

No description

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

No description

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

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "search",
    "department": "beer",
    "urls": [
        "https://www.danmurphys.com.au/beer/craft-beer",
        "https://www.danmurphys.com.au/whisky/scotch-whisky",
        "https://www.danmurphys.com.au/spirits",
        "https://www.danmurphys.com.au/whisky/all",
        "https://www.danmurphys.com.au/product/DM_560632/balter-xpa-cans-375ml"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/danmurphys-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",
    "department": "beer",
    "urls": [
        "https://www.danmurphys.com.au/beer/craft-beer",
        "https://www.danmurphys.com.au/whisky/scotch-whisky",
        "https://www.danmurphys.com.au/spirits",
        "https://www.danmurphys.com.au/whisky/all",
        "https://www.danmurphys.com.au/product/DM_560632/balter-xpa-cans-375ml",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/danmurphys-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",
  "department": "beer",
  "urls": [
    "https://www.danmurphys.com.au/beer/craft-beer",
    "https://www.danmurphys.com.au/whisky/scotch-whisky",
    "https://www.danmurphys.com.au/spirits",
    "https://www.danmurphys.com.au/whisky/all",
    "https://www.danmurphys.com.au/product/DM_560632/balter-xpa-cans-375ml"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/danmurphys-scraper --silent --output-dataset

```

## MCP server setup

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