# IKEA Scraper Lite (`abotapi/ikea-scraper-lite`) Actor

Scrape IKEA products across 50+ markets. Extract names, prices, currencies, ratings, full reviews, colours, dimensions, images, categories, variants, badges and store availability. Supports search, category, item-number and URL inputs with filters and sorting.

- **URL**: https://apify.com/abotapi/ikea-scraper-lite.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

## IKEA Scraper Lite: Products, Prices, Availability and Reviews

IKEA Scraper Lite turns any of IKEA's country storefronts into structured product data. Search by keyword, list a category, look up exact item numbers, or paste product, category, search or room/department links, and get back one flat record per product with pricing, ratings, colours, measurements, images, category path, variants and markdown status. Turn on detail enrichment for the full marketing description and individual customer reviews, and turn on availability for national and per-store stock, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **Four ways to find products.** Search by keyword, list a category, look up exact item numbers, or paste product, category, search and room/department links.
- **50+ country storefronts.** One Market and language field picks the country, currency and product range; a pasted link in URL mode carries its own market instead.
- **Specials and pricing built in.** Every product already carries its markdown info, was-price, savings amount, discount percent and IKEA's own specials label, at no extra request cost.
- **Room and department links just work.** A collection page like a living room hub is expanded into the real categories it lists, and each one is scraped, so you don't have to hand-build category links.
- **Detail enrichment on demand.** Attach the full description and individual customer reviews (author, date, title, text, rating) per product. It consumes additional platform usage only for the products it reads; there is no enrichment event fee.
- **Availability on demand.** Attach national stock and per-store stock, click and collect and cash and carry status per product. Pick the stores you care about (for example IKEA Logan) and get one stock entry per store, even when it holds none.
- **Built for monitoring.** Incremental mode returns only new and changed products on recurring runs, and a fully blocked search, category, or listing/room link fails loudly instead of returning an empty dataset. A blocked item-number lookup or a blocked pasted product link is not detected this way; see the FAQ below.

### Use Cases

- **Price and markdown tracking:** monitor a category or a list of item numbers for price drops, IKEA Family price markdowns and last-chance clearance.
- **Catalogue and range building:** pull a full category or room sweep into your own product database or marketplace feed.
- **Review and ratings research:** collect individual review text and ratings for sentiment analysis or competitor benchmarking.
- **Stock and fulfilment checks:** confirm national and per-store availability, click and collect and home delivery status before promoting a product.
- **Cross-market comparison:** compare price, currency and range differences for the same item numbers across several country storefronts.

### Data You Get

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

| Field | Example |
| --- | --- |
| `kind` | `"product"` |
| `id` / `itemNo` | `"00099999"` |
| `name` / `typeName` | `"SAMPLE"` / `"Shelving unit"` |
| `url` | `"https://www.ikea.com/us/en/p/sample-shelving-unit-white-00099999/"` |
| `price` / `currency` / `priceFormatted` | `148.0` / `"USD"` / `"$148.00"` |
| `onSale` / `isOnSpecial` | `true` / `true` |
| `wasPrice` / `discountPercent` / `specialsLabel` | `179.0` / `17.3` / `"IKEA Family price"` (null when the product carries no markdown) |
| `ratingValue` / `ratingCount` | `4.4` / `812` |
| `color` / `colors` | `"slate gray"` / `[{ "name": "slate gray", "id": "90512", "hex": "5b6470" }]` |
| `width` / `depth` / `height` / `measurementUnit` | `27.5` / `9.75` / `70.25` / `"in"` |
| `material` | `"Particleboard, foil, plastic edging"` |
| `categoryPath` | `["Storage & organization", "Shelving units"]` |
| `ancestorCategories` | `[{ "name": "Storage & organization", "key": "st001", "url": "https://www.ikea.com/us/en/cat/storage-organization-st001/" }]`, outermost first. `key` is IKEA's category id (usable in Category ids mode); the link opens that category. |
| `badge` / `isBestseller` | `"Best seller"` / `true` |
| `lastChance` / `onlineSellable` | `false` / `true` |
| `mainImage` / `mainImageAlt` / `images` | product photo URL(s) on ikea.com, and the photo's alt text |
| `contextualImageUrl` / `contextualImageAlt` | the in-room photo and its alt text (null when IKEA shows none) |
| `tag` / `tagText` / `features` | IKEA's listing tag and its label, e.g. `"FAMILY_PRICE"` / `"IKEA Family price"`, and the feature list when the listing carries one (null when none) |
| `numberOfVariants` / `variants` | `3` / sibling colour or finish options, each with its own price and image |
| `market` / `language` | `"us"` / `"en"` |
| `description` | full marketing copy (when detail enrichment is on) |
| `reviewsCollected` / `reviews` | `1` / each review's author, date, title, body and rating (when detail enrichment is on). Switch the dataset's **Reviews** view (next to Overview) to see one row per review instead of a nested array. |
| `availability` | national block (click and collect and home delivery are national signals, not per store) plus per-store stock (when availability is on) |
| `changeType` / `changedFields` | `"UPDATED"` / `["price"]` (incremental mode only) |
| `previousPrice` / `priceChanges` / `variantChanges` / `storeChanges` | the price before this run, and the old and new value of each moved price, variant and store field (compact update rows only) |

There is only one dataset record kind: a product. Reviews and availability are nested arrays/objects under it rather than separate rows, and `firstSeenAt` / `lastSeenAt` are added alongside `changeType` when incremental mode is on. With **Compact update rows** on, `UPDATED` and `EXPIRED` rows are short (see Resume and incremental mode below). The dataset's **Changes (incremental)** view shows `changeType`, `changedFields`, `previousPrice`, `priceChanges`, `variantChanges`, `storeChanges` and `otherChanges` side by side. Each run also leaves a `RUN_SUMMARY` record in its key-value store (linked from the Output tab, never a dataset row, never billed); see "Reading the results" below.

Each search or category walked in a run also leaves one `AVAILABLE_FILTERS` record in the run's key-value store (linked from the run's Output tab). It is not a dataset row and is never billed. It holds one entry per walked search or category: the `scope` (type, value, market, sort and the facets that were applied), the walked `category` (name, key, link, picture), `totalAvailable` (how many products IKEA lists there) and `availableFilters`, the listing's own filter panel as flat rows of `facet`, `facetParam`, `valueId`, `title`, `count` and `acceptedByFiltersInput` (true when that facet can be passed in the **Facet filters** input). It is read from the page the walk already fetched, so it costs no extra request.

### How to Use

1. Pick a **mode**: `search` (keywords), `productIds` (exact item numbers), `url` (paste product, category, search or room/department links, or a bare letter-prefixed category id), or `category` (legacy category-only mode, superseded by `url`).
2. Choose your **Market and language** (ignored in URL mode, since each link carries its own market).
3. Fill in the fields for that mode, apply any filters, and switch on detail enrichment or availability if you need them.
4. In `url` mode, leaving **URLs** untouched scrapes the built-in sample: eleven links that each mean something different (a product in two markets, a numeric and a letter-prefixed category, a filtered and sorted category, both search link forms with and without a sort, a room, a department hub, a bare category id, and an offers hub that is logged and skipped). Replace them with your own links for a real run.
5. Set **Max products** to control run size and cost, then click **Start**.

**Search for a category of products:**

```json
{
  "mode": "search",
  "market": "us/en",
  "searchTerms": ["bookcase"],
  "sortBy": "PRICE_LOW_TO_HIGH",
  "maxItems": 50,
  "fetchDetails": true,
  "maxReviews": 10,
  "includeAvailability": true
}
```

**List a category, no detail enrichment (cheaper, faster):**

```json
{
  "mode": "category",
  "market": "us/en",
  "categories": ["https://www.ikea.com/us/en/cat/bookcases-10382/"],
  "maxItems": 100,
  "fetchDetails": false
}
```

**Look up exact item numbers:**

```json
{
  "mode": "productIds",
  "market": "us/en",
  "productIds": ["20522046"],
  "fetchDetails": true,
  "includeAvailability": true
}
```

**Monitor a search link for changes on a schedule:**

```json
{
  "mode": "url",
  "urls": ["https://www.ikea.com/us/en/search/products/?q=bookcase"],
  "incrementalMode": true,
  "emitExpired": true,
  "maxItems": 100
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/ikea-scraper-lite").call(run_input={
    "mode": "search", "market": "us/en", "searchTerms": ["bookcase"],
})
for product in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(product["name"], product["price"], product["currency"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/ikea-scraper-lite').call({
  mode: 'search', market: 'us/en', searchTerms: ['bookcase'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

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

#### Markets, and mixing them in one run

Search, category and productIds modes all use one **Market and language** value for the whole run. In URL mode, each pasted link is read on its own: a link whose path starts with a valid two-letter country and two-letter language (for example `us/en` or `de/de`) uses that market instead of the field above, so one URL-mode run can genuinely mix several country storefronts. Keep in mind that filters like Min price and Max price compare against each record's own local currency number, so the same numeric threshold means a different real amount in a run that mixes, say, US dollars and Japanese yen.

#### Room and department links

A collection page such as a "rooms" or department hub is not itself a category, so the actor reads the category list that page publishes and scrapes each one. The run log names every category it walked, so you can see exactly what was covered. Products that a room page only features without listing a category for it are not included, so this is a broad sweep of the room rather than a pixel-perfect copy of the page. A page that publishes no category at all (an offers, new-arrivals or campaigns hub, or the bare room index) is reported in the log and skipped instead of being guessed as a keyword search.

#### Pasted links keep their own filters and sort

A search or category link pasted in URL mode keeps the `filters=` and `sort=` it carries, for example `.../cat/bookcases-10382/?filters=f-colors%3A10156&sort=PRICE_LOW_TO_HIGH`. Facets that match a **Facet filters** key (colors, materials, type, price, category) and any supported sort apply to that link only. When the same facet or the sort is also set in the input, the link wins for that link; facets the link does not mention still come from the input, and a link with no filters or sort simply uses the input's. Facets IKEA offers beyond those five keys are not applied and are named in the log. A page number in a link (`?page=2`) is not used: every link is walked from its first page and **Max products** sets how far.

#### Filters, sort and specials scope

The optional **Facet filters** object (`filters`) takes the site's own facet ids for `colors`, `materials`, `type`, `price` (e.g. `PRICE_0_10000`) and `category`, comma-separated, in search and category listings only; unknown keys are ignored. Min price, Max price, Min rating, In stock only, Contains keyword and the Specials facet all apply in search mode, category mode, and to a search or category link pasted in URL mode, exactly the same way. They are **not** applied to Product item numbers mode, or to a single product link pasted in URL mode: those always return that exact item, unfiltered.

#### Resume and incremental mode

- **Resume** (`resumeFromRunId`) skips product ids already collected by a previous run or dataset. For a fixed list of item numbers or pasted URLs, each id resolves on its own, so this always works cleanly. For a repeated **category or search** walk, the walk pages past the already-collected products as well as collecting the new ones you asked for, up to the site's own 400-product listing ceiling. Products ranked beyond that ceiling are not reachable by any run; narrow the search or change Sort by to reach them.
- **Incremental mode** (`incrementalMode`) is for running the same search, category, item-number list or URL set again and again (daily, weekly) and getting only the differences. Each product is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED` or `EXPIRED`. For search, category and listing/room links, a product is only ever marked absent, and can only ever come back as `REAPPEARED` later, after a run that had `emitExpired` on **and** completed a full, uncapped, non-Resume scan with no page refused or unreadable. The same holds for `productIds` mode and pasted single product links: a lookup that was refused or never answered counts as an unreadable scan, so those products are never marked `EXPIRED` (nor billed as such) on that run. `stateKey` names or shares one monitoring campaign's stored state; leave it empty to let the actor derive one from the market, mode and filters automatically. If another field in your setup turns out to be noisy (changes between runs without the product itself really changing; IKEA's `isBreathTaking` hot-deal flag or `priceValidFrom`/`priceValidTo` are candidates), list it in `ignoreFieldsForChanges` to exclude it from triggering `UPDATED`. Fields added to the record in later versions (such as `ancestorCategories` or `contextualImageUrl`) never count as a change on their own: a product saved by an earlier version stays `UNCHANGED` when only those fields are new, and its saved copy is brought up to date once. EXPIRED rows count toward **Max products**: once the run's remaining budget is used, the rest stay tracked as present and are returned on the next complete run (Max products `0` means no cap). A saved url-mode campaign whose links carry `filters=` or `sort=` starts a fresh baseline once, because those links are now scraped filtered and sorted as the page shows them. Adding a store to `storeIds` on a saved campaign returns each tracked product once as `UPDATED` with that store's stock (`old: null` in compact rows). A store whose stock a product has already returned, under any earlier selection, is never returned again just because it is selected again: removing, re-adding, reordering or alternating stores returns nothing by itself. A real stock change at a store that stays selected from one run to the next is reported as `UPDATED` with `availability` in `changedFields`, also in a run that adds or removes other stores. A change at a store that was not selected in the previous run is not reported when it is selected again (it was not being watched); only its changes from then on are. A selected store counts as returned once a stock answer for it has been returned, including an entry with `quantity` null and `inStock` false (the store does not list the item). `storeIds` is not part of the automatic state key, so two Tasks with the same search and different `storeIds` share one saved state; give them different State keys to keep them apart. When you start incremental mode together with Resume, the products that came from the resume source are tracked without their data (this run never read them). The first later run that reads them fills their saved copy silently: they are not returned and not billed, even with `emitUnchanged` on, and a real change after that is reported as `UPDATED` as usual. A change that happened between the resume source and that first run is therefore not reported. With incremental mode off, output is exactly one row per product, with no `changeType` field at all.
- **What counts as a change** (`changeScope`, incremental mode only). `all` (default) keeps the behaviour above: any change to the product record returns `UPDATED`. `priceAndStock` returns `UPDATED` only for price changes (`price`, `currency`, `wasPrice`, `savingsAmount`, `discountPercent`, `isOnSpecial`, `onSale`, and each colour/size variant's own `price`) and availability changes (`onlineSellable`, the product page's `offerAvailability`, the national click and collect and home delivery flags, and the in-stock, click and collect and cash and carry flags of each store chosen in `storeIds`); names, descriptions, images, badges, ratings and other details never do. `sellingPriceAndStock` works like `priceAndStock`, but a price change counts only when the selling price (`price`, for the product or a variant) or its currency moves; was prices, savings and special flags never return a row on their own. In both narrow options a store's unit count, its stock level label (`messageType`) and its aisle and bin never return a row on their own, and neither does the unfiltered top-stores list you get without `storeIds`. IKEA's listing gives each variant its own item number, price and currency but no stock of its own: to watch a variant's stock, add its item number to Product item numbers mode. Changing `changeScope` on a search with no `stateKey` continues from that search's saved baseline, so unchanged products are not returned or billed again. Each setting keeps its own saved baseline, though: switching back to a setting you already used before resumes that setting's older baseline, so a change that was already returned under the other setting since then is returned (and billed) once more. To avoid that, keep one setting per search, or set a `stateKey`, which keeps one baseline whatever the setting.
- **Compact update rows** (`compactUpdates`, incremental mode only). `UPDATED` and `EXPIRED` rows are short: `id`, `itemNo`, `url`, `name`, `price`, `currency`, `onlineSellable`, `offerAvailability` and the `availability` block, plus `previousPrice` (the price before this run, empty when it did not move), `priceChanges` (old and new of each moved price or availability field), `variantChanges` (one entry per colour/size variant that was `ADDED`, `REMOVED` or `CHANGED`, with old and new price) and `storeChanges` (one entry per chosen store whose availability moved, with old and new `quantity`, `inStock`, `clickAndCollect`, `cashAndCarry` and `messageType`, plus one `national: true` entry when the national click and collect or home delivery flag moved; without `storeIds`, the stores of the unfiltered top-stores list by store code; in **All fields**, also `aisle`, `bin` and `inRange`; a store or national block never known before is listed with `old: null`) and `otherChanges` (old and new of every other changed field no section above covers, for example `name` or `ratingCount`; the review list is given as its number of reviews). Every compact `UPDATED` row carries at least one of these sections. `NEW` rows always carry the complete record. Compact rows carry no review text, and they only report fields listed in `changedFields`: a field you list in `ignoreFieldsForChanges`, or one outside the chosen `changeScope`, never appears in `priceChanges`, `variantChanges` or `storeChanges` (ignoring `price` also leaves `previousPrice` empty). A compact row is billed exactly like the full row it replaces.
- **Expire after N missed full scans** (`expireAfterMissedScans`, with `emitExpired`). A product is marked `EXPIRED` only after it has been missing from this many complete scans in a row (default 1), so a product that briefly drops out of a listing is not reported as gone. A product that comes back resets the count. A capped, resumed or partly unreadable scan never counts as a miss.
- **Start from an existing dataset** (`baselineDatasetId`, optional, incremental mode only): if you already scraped the catalogue with this Actor, pick that run's dataset in the input form and your first incremental run returns only products that changed since then, instead of the whole catalogue again. Pick the dataset unedited: it must be this Actor's own product output (a CSV or Excel export, renamed columns or another tool's data is refused, and the run stops before scraping with a message saying why). Compact rows and `EXPIRED` rows in it are skipped; each product is seeded from its full row. It is only used while the state key has no saved state; once a run has completed it is ignored. Products in the dataset that are unchanged are not returned or billed; changes since the dataset was made come back as `UPDATED`, new products as `NEW`, and with `emitExpired` on, products no longer on the site as `EXPIRED`. It cannot be combined with Resume from a previous run, and it must be selected from the list (a pasted id may not be readable).
- **Coming from the IKEA Scraper?** (`earlierHistoryStoreId`, optional, incremental mode only). If you tracked the same State key (or the same search settings) there, pick that scraper's history store (`ikea-com-incremental-state`) in "Continue from the IKEA Scraper's history" and the first Lite run continues from that saved history, so you are not given the whole catalogue as NEW again. Your earlier history is only read, never changed, and nothing needs to be copied or run. The run stops with a message, before anything is scraped, when the picked store is not that scraper's history store, does not hold IKEA products, cannot be read, is too large, or holds saved history for this State key or these exact search settings in an unexpected format (nothing is partly imported). Without a State key, the history the IKEA Scraper saved for the same search under another **What counts as a change** setting is used too, the most recently saved one winning; history under such another setting that cannot be read is skipped with a warning while another one can be used, and stops the run only when none can. When this Actor has itself saved newer history for the same search under another setting, that newer history is used instead. When nothing is saved anywhere for this search, the run says so and starts fresh. It is ignored once the key already tracks products, and when Start from an existing dataset or Resume is set.
- **Missing products and unreadable checks.** A product that is no longer found is only marked `EXPIRED` when `emitExpired` is on, after `expireAfterMissedScans` complete scans in a row; a scan that hit Max products, used Resume, or could not read a page or a product lookup never marks anything `EXPIRED`. A stock check that could not be read this run is never reported as a change: the row (if anything else moved) shows `availability` empty for this run, `storeChanges` stays empty, and the last stock that was read is kept, so a stock change that happened in the meantime is reported on the next run that can read it. A product page that could not be read (description, material, review and rating fields, `offerAvailability`) is handled the same way: its fields are not compared that run, the last values that were read are kept, so a change made while the page was unreadable (for example `offerAvailability` going from `InStock` to `OutOfStock`) is reported on the next run that can read it, and that product's comparison simply skips those fields. This holds in every **What counts as a change** option, for full and compact rows, and however many runs in a row the page or stock check stays unreadable. Turning **Fetch full details** off is not an outage: those fields are then simply not compared. A stock answer or product page that has **never** been read for a tracked product (unreadable the first time the product was seen) is returned once as `UPDATED` the first time it is read (`old: null` in compact rows), so the first known stock is never lost; after that only real changes come back. A product whose stock lookup got no answer during the walk is read once more at the end of the run (up to 25 products per run), so it can appear later in the dataset than its listing position. When the stock service stops answering, the run reads each remaining product's stock once instead of retrying, and the run summary says how many could not be read.
- **What the availability fields mean.** `availability` empty (null): stock was not read this run, or the stock service does not know the item. A store entry with `quantity` 0: the store confirmed zero stock. A store entry with `quantity` null and `inStock` false: the store does not list the item. `changedFields` only names the fields that changed; the old and new values are in the compact sections (`priceChanges`, `variantChanges`, `storeChanges`, `otherChanges`). Combination products (item numbers starting with `s`, such as sofas and wardrobe combinations) get their stock like any other product.
- **Runs that reach their time limit.** A run stops looking up products about 75 seconds before its time limit, saves what it delivered, and says so in its status message; the products it did not reach are checked on the next run (no `EXPIRED` rows from such a run). Incremental state is also saved during the run (after every 200 returned products, at most every 30 seconds, and less often for very large searches so saving stays a small part of the run), so a run stopped without warning returns again on its next run only the products it had returned since its last save.
- **Very large monitoring campaigns.** The saved state of one search has a size limit. When a search grows past it, the run's status message says so and the state is not saved, so the next run returns those products again. Split such a search into smaller ones (by category, filter or market), each with its own **State key**.

### 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 product 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 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`, `category`, `productIds`, or `url`. |
| `market` | string | `us/en` | The IKEA market and language. Ignored per link in URL mode. |
| `searchTerms` | array | prefill `["bookcase"]` | Keywords to search (search mode). |
| `sortBy` | string | `RELEVANCE` | Order of results in a listing (search, category, or a listing link in URL mode). Includes Name Z-A and Customer rating. |
| `filters` | object | `{}` | Facet ids per key: `colors`, `materials`, `type`, `price`, `category`. |
| `categories` | array | prefill sample category link | Category listing URLs or bare ids (`category` mode, legacy; `url` mode now covers this too). |
| `productIds` | array | prefill sample item number | Exact IKEA item numbers (productIds mode). |
| `urls` | array | one example of every supported link shape (11 links) | Product, category or search links from ikea.com, or a bare letter-prefixed category id (url mode). A search or category link keeps its own filters and sort. |
| `minPrice` | integer | (none) | Only keep products at or above this price. |
| `maxPrice` | integer | (none) | Only keep products at or below this price. |
| `minRating` | integer | (none) | Only keep products with an average rating at or above this value (1-5). |
| `inStockOnly` | boolean | `false` | Keep only products currently sellable online. |
| `containsKeyword` | string | (none) | Only keep products whose name, type or description contains this text. |
| `specialsCategory` | string | `none` | Narrow a listing to `family_price` (IKEA Family price) or `last_chance`. |
| `fetchDetails` | boolean | `true` | Attach the full description and individual customer reviews. |
| `maxReviews` | integer | `10` | Cap on individual reviews captured per product (`0` disables review text), at most 100000. The default stays at 10 so existing incremental campaigns keep their saved state. Reviews consume additional platform usage; there is no enrichment event fee. The actual limit is however many reviews IKEA includes on the product page. |
| `includeAvailability` | boolean | `false` | Attach national and per-store stock per product. |
| `storeIds` | array | `[]` | Optional, needs `includeAvailability`. Numeric store ids or store names (`919` or `Logan` for IKEA Logan; names are known for Australia). Returns exactly these stores, one entry each, also at quantity 0 or when the item is not offered there. An unknown name stops the run before anything is collected. |
| `deliveryPostcode` | string | empty | Optional, market `au/en` only, needs `includeAvailability`. A 4-digit Australian postcode such as `4209`. Adds the postcode's home-delivery yes/no to `availability.national`, separate from the store quantities. Not a delivery date or price; stock at a store does not prove delivery. `null` in the answer means not confirmed this run (never `false`). Another format or market, or `includeAvailability` off, stops the run before anything is collected. |
| `maxItems` | integer | `20` | Total products to return across all inputs (the run cap). |
| `maxPages` | integer | `0` | Optional safety cap on result pages walked per search or category (`0` = no limit). |
| `proxy` | object | `{ "useApifyProxy": true }` | Connection settings. |
| `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 | `50` | Cap on products written to each connector per run. |
| `readProductsAtOnce` | integer | 4 | Product pages read in parallel (1-8) to finish faster. Output, order and charges are identical at any value; 1 reads one at a time. Reading drops back to one at a time automatically while the site pushes back. |
| `resumeFromRunId` | string | (none) | Continue one interrupted run without re-collecting or re-charging what it already has. |
| `incrementalMode` | boolean | `false` | Return only new and changed products on recurring runs. |
| `stateKey` | string | (none) | Name or share one monitoring campaign's incremental-mode state. |
| `baselineDatasetId` | string | (none) | Incremental mode only. Pick the dataset of an earlier run of this Actor to use as the starting point, so the first incremental run returns only changes. Not combinable with Resume. |
| `earlierHistoryStoreId` | string | (none) | Incremental mode only. Pick the IKEA Scraper's saved-history store (`ikea-com-incremental-state`) so the first Lite run for a State key, or for the same search settings, continues from that history instead of returning everything as NEW. Read only; used only while the key has no saved state, never with `baselineDatasetId` or Resume. |
| `ignoreFieldsForChanges` | array | `[]` | Extra field names excluded from `UPDATED` change detection, on top of the built-in handling (a description/review/availability fetch that simply didn't land never counts as a change). |
| `emitUnchanged` | boolean | `false` | Also return (and bill) unchanged products, marked `UNCHANGED`. |
| `emitExpired` | boolean | `false` | Also return (and bill) products no longer found, marked `EXPIRED`, once a run has fully scanned the tracked scope. EXPIRED rows count toward Max products; the rest stay tracked and come out on the next complete run (`0` = no cap). |
| `changeScope` | string | `all` | Incremental mode only. `priceAndStock` makes only price changes (including each variant's price) and availability changes (online sellable, national click and collect and home delivery, and in-stock, click and collect or cash and carry at the stores in `storeIds`) return `UPDATED`. `sellingPriceAndStock` counts a price change only when the selling price moves. Unit counts and stock level labels alone never return a row in either. Changing it on a search with no `stateKey` continues from the saved baseline. |
| `compactUpdates` | boolean | `false` | Incremental mode only. `UPDATED` and `EXPIRED` rows are short, with `previousPrice`, `priceChanges`, `variantChanges` and `storeChanges` (old and new of each moved field). `NEW` rows always carry the complete record. Billed exactly like full rows. |
| `expireAfterMissedScans` | integer | `1` | Incremental mode only, with `emitExpired`. A product is marked `EXPIRED` only after it has been missing from this many complete scans in a row (1 to 30). |

#### Store stock for chosen stores

With `includeAvailability` on and `storeIds` set (for example `["919"]` for IKEA Logan), `availability.stores` holds exactly the stores you listed, one entry each and sorted by store code (the order you typed them in does not matter):

```json
{
  "availability": {
    "storesWithStock": 1,
    "stores": [
      {
        "storeId": "919",
        "storeName": "Logan",
        "quantity": 176,
        "inStock": true,
        "clickAndCollect": true,
        "cashAndCarry": true,
        "messageType": "HIGH_IN_STOCK",
        "aisle": "16",
        "bin": "01",
        "inRange": true
      }
    ]
  }
}
```

A store that does not offer the item (missing from the stock answer, or answered without a count) still gets an entry with `quantity` null, `inStock` false and `inRange` false; a store that reports a count of 0 shows `quantity` 0. `aisle` and `bin` are filled when the store publishes the in-store location. Delivery PRICE and delivery DATE are not available. `availability.national` says whether home delivery is offered in the country; set `deliveryPostcode` to get a postcode-specific yes/no instead (see Home delivery to a postcode).

#### Home delivery to a postcode

Set `deliveryPostcode` (a 4-digit Australian postcode such as `4209`) to also answer "can IKEA home-deliver this product to that postcode" for every product, separately from the store quantities. It needs market `au/en` and Include store availability; any other setup, or a value that is not four digits, stops the run before anything is collected. It adds no stock reads in the normal case: the postcode rides on the stock read the run already makes. If IKEA does not recognise the postcode, the first such answer switches the postcode off for the rest of the run (delivery stays `null`, with one warning), so only the few reads already under way at that moment (no more than the number of products read at once, plus one) are repeated, once, without the postcode.

Recommended setup for "Logan store stock plus delivery to 4209" on a schedule:

```json
{
  "mode": "productIds",
  "market": "au/en",
  "productIds": ["00099999", "10099999"],
  "includeAvailability": true,
  "storeIds": ["919"],
  "deliveryPostcode": "4209",
  "incrementalMode": true,
  "changeScope": "priceAndStock",
  "compactUpdates": true
}
```

With category mode the same three availability fields work unchanged. In each row, `availability.stores` is the Logan stock as before, and `availability.national` becomes the postcode's answer:

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

```json
"national": { "country": "AU", "clickCollect": true, "homeDelivery": true, "clickCollectInRange": true, "homeDeliveryInRange": true, "stockStatus": "HIGH_IN_STOCK", "deliveryPostcode": "4209" }
```

- `deliveryPostcode`: the postcode the answer was read for. The key is only present when you set the input; without it the `national` block is exactly as before (the country-wide delivery flag).
- `homeDelivery` / `homeDeliveryInRange`: `true` when IKEA offers home delivery to that postcode, `false` when it does not. This is a yes/no, not a delivery date or a delivery price (neither is available). Stock at a store does not prove delivery, and delivery to a postcode does not prove store stock.
- **`null` means "not confirmed this run, keep your previous value", never `false`.** It appears when the answer could not be read (the stock read failed) or when IKEA does not recognise the postcode. For an unrecognised postcode that product is read again without it, so the store stock still arrives; all later products are read without the postcode, and one warning is logged per run.
- In incremental mode a flip such as `true` to `false` is an ordinary change of `availability.national` (`UPDATED`, and a `national: true` entry in `storeChanges` of compact rows). A `null` is never a change and never replaces the last known answer; the first known answer after `null` is reported once with `old: null`. Setting, changing or clearing the postcode never returns the catalogue again: the answer saved for another postcode is not compared, the new answer and postcode simply become the saved baseline without a row, and later changes are compared against the answer for the same postcode. The postcode is not part of the automatic state key.
- `RUN_SUMMARY` gains `deliveryReads` (`attempted`, `answered`, `unreadable`) next to `stockReads`, only when the input is set.

IKEA does not publish an EAN barcode through this actor.

#### Daily price and store stock monitor

Schedule a daily run with this input (your item numbers, or search, category or product links in URL mode):

```json
{
  "mode": "productIds",
  "market": "au/en",
  "productIds": ["00099999", "10099999"],
  "includeAvailability": true,
  "storeIds": ["919"],
  "incrementalMode": true,
  "changeScope": "sellingPriceAndStock",
  "compactUpdates": true,
  "emitExpired": true,
  "expireAfterMissedScans": 3,
  "stateKey": "logan-daily",
  "fetchDetails": false,
  "maxItems": 0
}
```

The first run returns every product in full as `NEW`. Later runs return a product only when its selling price (or a colour/size variant's price) moved, or when it went in or out of stock, click and collect or cash and carry at IKEA Logan, or online sellability or national click and collect or home delivery changed. Unchanged products are not returned and not billed. A compact row looks like this:

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

```json
{
  "kind": "product",
  "id": "00099999",
  "itemNo": "00099999",
  "url": "https://www.ikea.com/au/en/p/sample-shelving-unit-white-00099999/",
  "name": "SAMPLE",
  "currency": "AUD",
  "price": 129.0,
  "onlineSellable": true,
  "availability": {
    "national": { "country": "AU", "clickCollect": true, "homeDelivery": true, "clickCollectInRange": true, "homeDeliveryInRange": true, "stockStatus": "HIGH_IN_STOCK" },
    "storesWithStock": 0,
    "stores": [
      { "storeId": "919", "storeName": "Logan", "quantity": 0, "inStock": false, "clickAndCollect": false, "cashAndCarry": false, "messageType": "OUT_OF_STOCK", "aisle": "16", "bin": "01", "inRange": true }
    ]
  },
  "changeType": "UPDATED",
  "changedFields": ["availability", "price"],
  "firstSeenAt": "2026-11-01T00:00:00Z",
  "lastSeenAt": "2026-11-03T00:00:00Z",
  "previousPrice": 149.0,
  "priceChanges": { "price": { "old": 149.0, "new": 129.0 } },
  "variantChanges": null,
  "storeChanges": [
    {
      "storeId": "919",
      "storeName": "Logan",
      "national": false,
      "country": null,
      "changes": {
        "quantity": { "old": 12, "new": 0 },
        "inStock": { "old": true, "new": false },
        "clickAndCollect": { "old": true, "new": false },
        "cashAndCarry": { "old": true, "new": false },
        "messageType": { "old": "HIGH_IN_STOCK", "new": "OUT_OF_STOCK" }
      }
    }
  ],
  "otherChanges": null
}
```

With `expireAfterMissedScans` at 3, an item that IKEA stops listing comes back once as a short `EXPIRED` row after three daily runs in a row without it. Keep `fetchDetails` on if you also want the product page's own stock status (`offerAvailability`) compared; it consumes additional platform usage, with no enrichment event fee.

### Output Example

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

```json
{
  "kind": "product",
  "id": "00099999",
  "itemNo": "00099999",
  "itemType": "ART",
  "name": "SAMPLE",
  "typeName": "Shelving unit",
  "description": "A simple sample shelving unit with adjustable shelves.",
  "url": "https://www.ikea.com/us/en/p/sample-shelving-unit-white-00099999/",
  "price": 148.0,
  "currency": "USD",
  "priceFormatted": "$148.00",
  "onSale": true,
  "isOnSpecial": true,
  "wasPrice": 179.0,
  "wasPriceFormatted": "$179.00",
  "savingsAmount": 31.0,
  "discountPercent": 17.3,
  "specialsLabel": "IKEA Family price",
  "ratingValue": 4.4,
  "ratingCount": 812,
  "averageRating": 4.4,
  "reviewCount": 812,
  "color": "slate gray",
  "colors": [{ "name": "slate gray", "id": "90512", "hex": "5b6470" }],
  "measurementText": "27 1/2x9 3/4x70 1/4 \"",
  "width": 27.5,
  "depth": 9.75,
  "height": 70.25,
  "measurementUnit": "in",
  "material": "Particleboard, foil, plastic edging",
  "categoryPath": ["Storage & organization", "Shelving units"],
  "ancestorCategories": [
    { "name": "Storage & organization", "key": "st001", "url": "https://www.ikea.com/us/en/cat/storage-organization-st001/" },
    { "name": "Shelving units", "key": "st002", "url": "https://www.ikea.com/us/en/cat/shelving-units-st002/" }
  ],
  "badge": "Best seller",
  "isBestseller": true,
  "onlineSellable": true,
  "mainImage": "https://www.ikea.com/us/en/images/products/sample__0000001.jpg",
  "mainImageAlt": "SAMPLE Shelving unit, slate gray",
  "contextualImageUrl": "https://www.ikea.com/us/en/images/products/sample__0000002.jpg",
  "contextualImageAlt": "Living room with the SAMPLE shelving unit",
  "tag": "FAMILY_PRICE",
  "tagText": "IKEA Family price",
  "images": ["https://www.ikea.com/us/en/images/products/sample__0000001.jpg"],
  "numberOfVariants": 3,
  "variants": [
    { "id": "00099998", "name": "SAMPLE", "color": "black", "price": 158.0, "currency": "USD" }
  ],
  "market": "us",
  "language": "en",
  "reviewsCollected": 1,
  "reviews": [
    {
      "author": "Jordan R.",
      "date": "2026-07-14",
      "title": "Sturdy and easy to build",
      "body": "Took about an hour to assemble and looks great in the living room.",
      "rating": 5
    }
  ],
  "availability": {
    "national": { "country": "US", "homeDelivery": true, "clickCollect": true, "stockStatus": "HIGH_IN_STOCK" },
    "storesWithStock": 37,
    "stores": [
      { "code": "512", "cashCarry": true, "quantity": 48, "stockStatus": "HIGH_IN_STOCK" }
    ]
  }
}
```

### Plan Requirement

IKEA's product and listing web pages sit behind an edge filter that refuses datacenter connections (HTTP 403 "Attention Required" page), while the same pages load normally over a residential connection. Search, category and availability data are read from lighter data endpoints first, and the run automatically retries with a different connection when one is refused, including a backup connection when the primary one is exhausted. Detail enrichment (the full description and individual review text) needs a residential connection and is skipped without one, so on a plan without residential proxy access expect results without descriptions or reviews. Pasted product links and pages that only load in full over residential are also affected. Choose a residential proxy group in the proxy settings for full coverage.

### FAQ

#### How much does it cost?

You pay one product event per returned dataset row and one run-start event per run, plus Apify platform usage for compute, proxies, storage and data transfer. The Pricing tab shows the current rates for your plan. This Lite version has no enrichment or review event fees. Fetching full details and reviews, and store availability, still consume platform resources. Use **Max products**, **Max pages**, **Max reviews per product** and the platform run limits to bound work.

If you set a maximum total charge for a run, the run stops looking up products once that limit leaves room for no further product, and says so in its status message. Only products actually saved to the dataset are billed. In incremental mode a new or changed product (or an `EXPIRED` one) that did not fit under the limit is not marked as delivered: it is returned on the next run, so no change is lost and none is returned twice. A run stopped by the limit never marks products `EXPIRED`.

#### Reading the results

Every run writes a `RUN_SUMMARY` record to its key-value store: `productsScanned`, `productsReturned` (including `EXPIRED` rows; `expiredRowsReturned` gives their number), `rowsByChangeType`, `stockReads` (`attempted`, `answered`, `unknownToStockService`, `unread`), `deliveryReads` (`attempted`, `answered`, `unreadable`; only with `deliveryPostcode`), `productPages` (`attempted`, `landed`, `unread`), `scanComplete` (the same test that allows `EXPIRED` rows), `stateCommitted` and `stateProblem` (incremental mode), `stoppedEarlyReason` (`maxItems`, `maxTotalCharge`, `timeLimit` or empty) and `stoppedAtChargeLimit`. When fewer than 99% of the stock or product-page reads could be read, the run's status message says how many.

#### What changed in how many rows a run returns (November 2026)

These fixes change how many rows incremental runs return:

- Combination products (item numbers starting with `s`) now get their stock. Each tracked one is returned once as `UPDATED` the first time its stock is read; in furniture searches this can be a large share of the tracked products, once.
- A tracked product whose stock or product page had never been read is returned once as `UPDATED` when it first is.
- Adding a store to `storeIds` on a saved search returns each tracked product once with that store's stock, once per store ever: removing, re-adding, reordering or alternating stores returns nothing.
- A colour/size variant list that only changes order is no longer returned as `UPDATED` (fewer rows).
- With a fixed **State key**, turning **Include availability** or **Fetch full details** off and back on returns nothing by itself when the option was turned off with this release; only real changes come back. State saved by an earlier release while the option was off does not record that it was off, so the first run with the option back on returns each tracked product once as `UPDATED` with the newly read fields (for example `description`, `material`, `offerAvailability` or `availability`). (Without a State key these two options are part of the search's saved settings, so changing them starts a separate baseline, as before.)

#### Is it legal to scrape IKEA?

This actor collects only publicly available product data. You are responsible for how you use it: follow IKEA's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Product names, images and marketing descriptions may be subject to IKEA's own rights.

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

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new and updated products, and unchanged ones are not billed unless you also turn on **Emit unchanged**. Use **What counts as a change** to watch only prices and stock, and **Compact update rows** to get the old and new values instead of the full record (see "Daily price and store stock monitor" above).

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

For search, category and listing/room links, if every page this run tried to read was refused or never answered, the run stops with a clear error instead of finishing with 0 products, so "nothing found" is never confused with "nothing could be read". A genuinely empty listing, or a category id IKEA does not recognise, still finishes normally with 0 products and a status message explaining which case it was. For `productIds` mode and pasted single product links, a fully blocked run is not currently distinguished from a genuinely empty result: it finishes normally with 0 products and a generic "no products found" message, so treat a surprising empty result from a lookup as worth retrying before assuming the item numbers themselves are wrong.

#### Why is Resume returning nothing new against a category I've already collected a lot from?

Resume skips ids you already have and pages past them, but IKEA caps any listing at 400 products. Once you hold that many, narrow the search or change Sort by.

#### 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, Linear or Airtable.

### 🔗 Want more e-commerce data?

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

<table>
<tr><td>🛒 <a href="https://apify.com/abotapi/target-au-scraper"><b>Target AU Scraper</b></a><br>Scrape products and customer reviews from Target.com.au. Search by keyword or use...</td><td>🛒 <a href="https://apify.com/abotapi/muji-scraper"><b>MUJI Scraper</b></a><br>Scrape MUJI products from the United States, Canada and Australia storefronts. Search by...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/kmart-au-scraper"><b>Kmart Scraper</b></a><br>Scrape products and customer reviews from Kmart.com.au. Search by keyword or use...</td><td>🛒 <a href="https://apify.com/abotapi/myer-au-scraper"><b>Myer Scraper</b></a><br>Scrape Myer (myer.com.au) department-store products: name, brand, price, was-price...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/harvey-norman-scraper"><b>Harvey Norman Scraper</b></a><br>Scrape Harvey Norman Australia products: name, brand, price, was-price / discount...</td><td>🛒 <a href="https://apify.com/abotapi/costco-au-scraper"><b>Costco AU Scraper</b></a><br>Scrape Costco.com.au products by keyword or product, search and category URLs. Extract...</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/ikea-scraper-lite/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 = keywords + filters. productIds = exact item numbers. url = paste product, category or search links, or bare category ids, and walk them.

## `market` (type: `string`):

The IKEA market (and its language) to scrape. Sets prices, currency, product range and the site language. In URL mode the market is taken from each link instead.

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

One or more keywords to search, e.g. sofa, bookcase, desk lamp.

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

Order of results within each search or category listing, including a search or category link pasted in URL mode. Ignored for productIds and single product links.

## `productIds` (type: `array`):

Exact IKEA item numbers, e.g. 20522046 — each resolves to a full product record.

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

Paste product, category or search links from ikea.com, or a bare letter-prefixed category id (fu003, tl001, bm003) — each link's own market is used, defaulting to Market and language above for a bare id. A search or category link keeps the filters and sort it carries (for example ?filters=f-colors%3A10156\&sort=PRICE_LOW_TO_HIGH): they apply to that link and take precedence over the same Facet filters key or Sort by chosen below; a page number (?page=2) is not used, every link is walked from its first page. A room or department link (e.g. ikea.com/us/en/rooms/living-room/) expands into every category that page itself lists; a link with no resolvable category (offers, new, campaigns) is reported in the log and skipped, never guessed as a keyword. Every other field in the Filters section below still applies here, to each search or category link you paste; a single product link always returns that exact item, unfiltered.

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

Deprecated compatibility field for saved tasks and API integrations using mode="category". New runs should use URL mode above, which accepts the same category links and bare ids directly.

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

Only keep products at or above this price (market currency).

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

Only keep products at or below this price (market currency).

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

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

## `inStockOnly` (type: `boolean`):

Keep only products currently sellable online.

## `containsKeyword` (type: `string`):

Keep only products whose name, type or description contains this text.

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

Show only products in one of IKEA's specials categories: IKEA Family price (loyalty-program markdown) or Last chance (clearance / limited-stock items). Off by default. Added as a facet to the same listing request.

## `filters` (type: `object`):

Native IKEA facet filters for search and category listings. Keys: colors, materials, type, price, category; values are comma-separated facet ids as shown on the site's filter panel (colors e.g. 10139, price e.g. PRICE_0_10000). Unknown keys are ignored. Not applied in item-number mode or to single product links.

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

Attach the full marketing description and individual customer reviews (title, text, author, date, rating) to each product.

## `maxReviews` (type: `integer`):

Cap on individual reviews captured per product (0 disables review text; ratings are still attached). Reviews consume additional platform usage; there is no enrichment event fee. The true limit is however many reviews IKEA includes on the product page itself; values above 100000 are treated as 100000.

## `includeAvailability` (type: `boolean`):

Attach national and per-store stock (quantity, click & collect, cash & carry) to each product. Leave Store IDs empty for the stores that currently hold stock (up to 25).

## `storeIds` (type: `array`):

Needs Include store availability switched on. Return stock for only these stores, always one entry per store you list, even when the quantity is 0 or the item is not offered there. Use the numeric store id (919 is IKEA Logan) or a store name such as Logan (names are known for Australia). An unknown name stops the run before anything is collected.

## `deliveryPostcode` (type: `string`):

Only for market Australia / English; needs Include store availability switched on. Adds a postcode-specific yes/no to each product: does IKEA offer home delivery to this 4-digit Australian postcode (for example 4209). It is reported next to, and separately from, the store quantities. It is not a delivery date or a delivery price, and stock at a store does not prove delivery. Leave empty to skip. A postcode in another format, another market, or Include store availability off stops the run before anything is collected.

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

Total products to return across all inputs — the overall run cap.

## `readProductsAtOnce` (type: `integer`):

How many product pages are read in parallel to finish faster. Results, order and charges are identical at any value. Set 1 to read strictly one at a time. Reading automatically drops back to one at a time whenever the site pushes back.

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

Optional safety cap on result pages walked per search or category. Leave empty or 0 for no page limit — the run stops at Max products instead.

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

Apify Proxy configuration. Keeping Apify Proxy enabled is recommended for reliable results. Attaching the full description and individual review texts works best on a residential connection and may be skipped on limited plans.

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

Paste a previous run ID or dataset ID to continue a large crawl of products without returning or charging for products already collected there. Use this after an interrupted run, or when continuing a product catalogue pull in another run. For recurring monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring. The first run returns all matching products as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED products. Turn on "Emit unchanged" or "Emit expired" only when you also want those products returned (and billed). State is kept separately per market, mode (search/category/productIds/url), and filter setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

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

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

## `baselineDatasetId` (type: `string`):

Optional. Pick the dataset of an earlier full run of this Actor to use as your starting point, so your first incremental run returns only products that changed since then instead of the whole catalogue again. Pick the dataset unedited: it must be this Actor's own product output. Only used on the first run for a state key; ignored once that state key already tracks products. Cannot be combined with Resume from a previous run. If the dataset is old, everything that changed since is returned as UPDATED or NEW.

## `earlierHistoryStoreId` (type: `string`):

Optional. Pick the saved-history store of the IKEA Scraper (named ikea-com-incremental-state) so your first Lite run for a State key, or for the same search settings, continues from the history that scraper already tracked, instead of returning the whole catalogue as NEW. The store is only read, never changed. Only used on the first run for a state key; ignored once that state key already tracks products, or when Start from an existing dataset or Resume is set.

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

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

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

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

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

All fields (default): any change to the product record returns an UPDATED row, exactly as before this option existed. Price and availability only: only price changes (price, was price, savings, discount and special flags, and each colour/size variant's own price) and availability changes (online sellable, the product page's stock status, national click and collect and home delivery, and in-stock, click and collect or cash and carry changes at the stores chosen in Store ids) return an UPDATED row; names, descriptions, images, badges, ratings and other details never do. Selling price and availability only: like Price and availability only, but a price change counts only when the selling price (price, for the product or a variant) or its currency moves; was prices, savings and special flags never return a row on their own. In both narrow options, unit counts, the stock level label and the in-store location alone never return a row. Changing this option on a search with no State key continues from that search's saved baseline instead of starting over.

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

Off by default. When on, UPDATED rows (and EXPIRED rows) are short: product id, url, name, current price, online sellable flag and the availability block, plus priceChanges, variantChanges (per colour/size variant) and storeChanges (per chosen store, and the national click and collect and home delivery flags) showing the old and new value of each moved field. NEW rows always carry the complete product record. Review text is not included in compact rows. A compact row is billed exactly like the full row it replaces.

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

Only used with Emit expired products. A product is marked EXPIRED only after it has been missing from this many full scans in a row (default 1). Raise it (e.g. 3) so a product that briefly drops out of a search or category listing is not reported as gone. A product that comes back resets the count. A scan that was capped, resumed or could not read a page never counts as a miss. Stock status itself is reported through the availability fields, not through EXPIRED.

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

Optional. Field names to exclude from UPDATED change detection, in addition to the built-in handling (full description, reviews and store availability already never trigger a false UPDATED when their own fetch simply didn't land this run). Use this if another field in your setup turns out to be noisy — for example priceValidFrom/priceValidTo or isBreathTaking, which IKEA can rotate without the product itself changing.

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

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify then Settings then API & Integrations, then select it here. Notion gets a page-per-item export; other connectors get a best-effort write. Leave empty to skip. Supported: Notion, Linear, Airtable, Apify.

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

URL or id of the Notion page under which item pages are created. Required only for the Notion export.

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

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

## Actor input object example

```json
{
  "mode": "search",
  "market": "us/en",
  "searchTerms": [
    "bookcase"
  ],
  "sortBy": "RELEVANCE",
  "productIds": [
    "20522046"
  ],
  "urls": [
    "https://www.ikea.com/us/en/p/billy-bookcase-white-20522046/",
    "https://www.ikea.com/gb/en/p/billy-bookcase-white-00263850/",
    "https://www.ikea.com/us/en/cat/bookcases-10382/",
    "https://www.ikea.com/us/en/cat/chairs-fu002/",
    "https://www.ikea.com/us/en/cat/bookcases-10382/?filters=f-colors%3A10156&sort=PRICE_LOW_TO_HIGH",
    "https://www.ikea.com/us/en/search/products/?q=bookcase",
    "https://www.ikea.com/us/en/search/?q=bookcase&sort=PRICE_LOW_TO_HIGH",
    "https://www.ikea.com/us/en/rooms/living-room/",
    "https://www.ikea.com/us/en/products/storage-furniture/bookcases/",
    "https://www.ikea.com/us/en/offers/",
    "fu003"
  ],
  "categories": [
    "https://www.ikea.com/us/en/cat/bookcases-10382/"
  ],
  "inStockOnly": false,
  "specialsCategory": "none",
  "filters": {},
  "fetchDetails": true,
  "maxReviews": 10,
  "includeAvailability": false,
  "storeIds": [],
  "maxItems": 20,
  "readProductsAtOnce": 4,
  "maxPages": 0,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "changeScope": "all",
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

## `reviews_view` (type: `string`):

No description

## `availableFilters` (type: `string`):

No description

## `runSummary` (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": [
        "bookcase"
    ],
    "productIds": [
        "20522046"
    ],
    "urls": [
        "https://www.ikea.com/us/en/p/billy-bookcase-white-20522046/",
        "https://www.ikea.com/gb/en/p/billy-bookcase-white-00263850/",
        "https://www.ikea.com/us/en/cat/bookcases-10382/",
        "https://www.ikea.com/us/en/cat/chairs-fu002/",
        "https://www.ikea.com/us/en/cat/bookcases-10382/?filters=f-colors%3A10156&sort=PRICE_LOW_TO_HIGH",
        "https://www.ikea.com/us/en/search/products/?q=bookcase",
        "https://www.ikea.com/us/en/search/?q=bookcase&sort=PRICE_LOW_TO_HIGH",
        "https://www.ikea.com/us/en/rooms/living-room/",
        "https://www.ikea.com/us/en/products/storage-furniture/bookcases/",
        "https://www.ikea.com/us/en/offers/",
        "fu003"
    ],
    "categories": [
        "https://www.ikea.com/us/en/cat/bookcases-10382/"
    ],
    "storeIds": [],
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "changeScope": "all",
    "compactUpdates": false,
    "expireAfterMissedScans": 1,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/ikea-scraper-lite").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": ["bookcase"],
    "productIds": ["20522046"],
    "urls": [
        "https://www.ikea.com/us/en/p/billy-bookcase-white-20522046/",
        "https://www.ikea.com/gb/en/p/billy-bookcase-white-00263850/",
        "https://www.ikea.com/us/en/cat/bookcases-10382/",
        "https://www.ikea.com/us/en/cat/chairs-fu002/",
        "https://www.ikea.com/us/en/cat/bookcases-10382/?filters=f-colors%3A10156&sort=PRICE_LOW_TO_HIGH",
        "https://www.ikea.com/us/en/search/products/?q=bookcase",
        "https://www.ikea.com/us/en/search/?q=bookcase&sort=PRICE_LOW_TO_HIGH",
        "https://www.ikea.com/us/en/rooms/living-room/",
        "https://www.ikea.com/us/en/products/storage-furniture/bookcases/",
        "https://www.ikea.com/us/en/offers/",
        "fu003",
    ],
    "categories": ["https://www.ikea.com/us/en/cat/bookcases-10382/"],
    "storeIds": [],
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "changeScope": "all",
    "compactUpdates": False,
    "expireAfterMissedScans": 1,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/ikea-scraper-lite").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": [
    "bookcase"
  ],
  "productIds": [
    "20522046"
  ],
  "urls": [
    "https://www.ikea.com/us/en/p/billy-bookcase-white-20522046/",
    "https://www.ikea.com/gb/en/p/billy-bookcase-white-00263850/",
    "https://www.ikea.com/us/en/cat/bookcases-10382/",
    "https://www.ikea.com/us/en/cat/chairs-fu002/",
    "https://www.ikea.com/us/en/cat/bookcases-10382/?filters=f-colors%3A10156&sort=PRICE_LOW_TO_HIGH",
    "https://www.ikea.com/us/en/search/products/?q=bookcase",
    "https://www.ikea.com/us/en/search/?q=bookcase&sort=PRICE_LOW_TO_HIGH",
    "https://www.ikea.com/us/en/rooms/living-room/",
    "https://www.ikea.com/us/en/products/storage-furniture/bookcases/",
    "https://www.ikea.com/us/en/offers/",
    "fu003"
  ],
  "categories": [
    "https://www.ikea.com/us/en/cat/bookcases-10382/"
  ],
  "storeIds": [],
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "changeScope": "all",
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/ikea-scraper-lite --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/ikea-scraper-lite"
        }
    }
}
```

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/zUdQbYHLiBdjPMwdx/builds/aH59m4FXTaZ2mLoDV/openapi.json
