# Etsy Scraper: Listings, Prices, Variations & Reviews (`abotapi/etsy-marketplace-scraper`) Actor

Scrape Etsy listings by keyword, category, or URL. Extract titles, shops, prices, discounts, availability, images, categories, and variations. Optional detail mode adds descriptions, materials, shipping origin, and item-level reviews. Supports incremental monitoring.

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

## Pricing

from $1.00 / 1,000 listing results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

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

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

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

# README

## Etsy Marketplace Scraper

Scrape listings from **Etsy** (`etsy.com`) — the handmade, vintage and craft
marketplace. Search by keyword and/or category, or paste links directly. Every
listing comes back as one flat record with title, shop, price, was‑price/
discount, availability, images, category and variations; enable details to add
the full description, materials, shipping origin, rating breakdown and
item‑level reviews.

### What you can do

- **Search by keyword and/or category** — e.g. `handmade necklace`, or scope to
  a category path like `jewelry/necklaces` — with Etsy's own price, on‑sale,
  free‑shipping and personalizable filters, plus sorting.
- **Paste links** — mix listing links, category/search links and shop links in
  one list and let the actor route each entry. Listing links return full
  detail; category/search/shop links are walked across their result pages.
- **Resume & recurring updates** — turn on Incremental mode to get only NEW,
  UPDATED, and REAPPEARED listings on every scheduled run, or resume one
  specific interrupted crawl with `resumeFromRunId`.

### Input

| Field | Description |
|---|---|
| **Mode** | `search` or `url`. |
| **Search keywords** | Keywords to search (search mode). Each is searched separately. Leave empty and set only Category path to browse a whole category. |
| **Category path** | Optional Etsy category path to scope the search or browse to, e.g. `jewelry/necklaces` (search mode). |
| **Etsy links** | Listing links, bare listing ids, category/search links, or shop links to scrape directly, mixed freely (url mode). Listing links return full detail. |
| **Sort by** | Relevancy, price ↑/↓, newest first, or highest reviews first. |
| **On sale only** | Keep only listings currently on sale. |
| **Free shipping only** | Keep only listings with free shipping. |
| **Personalizable only** | Keep only listings that can be personalized. |
| **Min / Max price (USD)** | Keep only listings within a price range. |
| **Ships to** | Two-letter country code to only show listings that ship there. |
| **Fetch listing details** | Add description, materials, category breadcrumbs, extra images, shipping origin, variations, rating breakdown and item-level reviews. On by default. |
| **Max listings** | The run's cap on how many listings to return. Default 20; `0` = unlimited. |
| **Max pages per keyword / link** | Result pages walked per keyword / category / link. Default `0` = unlimited — walks every page until *Max listings* is hit or a page repeats no new listings. |
| **Resume from a previous run** | Continue one specific previous run/dataset: listings already collected there are skipped, so this run only appends new listings. For recurring daily monitoring of the same search, use Incremental mode instead — see "Resume & recurring updates" below. |
| **Incremental changes for scheduled runs** | Daily/recurring monitoring of this same search. First run returns everything as `NEW`; later runs return only `NEW`/`UPDATED`/`REAPPEARED` by default. See "Resume & recurring updates" below. |
| **State key** | Optional name for a monitoring campaign, so its incremental state stays stable or is deliberately shared. Auto-derived from your search/detail settings when left empty. |
| **Emit unchanged listings** | Incremental mode only. Also return listings unchanged since the last run, marked `UNCHANGED`. Adds and bills extra rows you already have. |
| **Emit expired listings** | Incremental mode only. Also return listings from a previous run no longer found, marked `EXPIRED`, once a run has fully scanned the search (not capped, not a resume). Adds and bills extra synthetic rows. |
| **Proxy** | Apify Proxy — a residential exit recommended (see below). |
| `mcpConnectors` | Optional MCP connectors to export results into (Notion, Linear, Airtable, Apify). |
| `notionParentPageUrl` | Notion connector only: page under which item pages are created. |
| `maxNotifyListings` | Cap on items exported to each connector per run. Does not affect the dataset. |

### Output

One record per listing. With details off you get the search-card fields; with
details on you also get description, materials, category, shipping origin,
variations, rating breakdown and item-level reviews. Example (illustrative
values):

```json
{
  "listingId": "4530068104",
  "title": "Example personalized initial keychain",
  "shopId": "14528878",
  "shopName": "ExampleCraftStudio",
  "shopUrl": "https://www.etsy.com/shop/ExampleCraftStudio",
  "categoryPath": ["Accessories", "Keychains & Lanyards", "Keychains"],
  "price": 16.0,
  "currency": "USD",
  "originalPrice": null,
  "discountPercentage": null,
  "onSale": false,
  "availability": "in_stock",
  "quantityAvailable": 38,
  "images": ["https://i.etsystatic.com/example/il_fullxfull.example.jpg"],
  "videoUrl": null,
  "description": "Full listing description...",
  "materials": ["Cotton", "Felt"],
  "shippingOrigin": { "addressCountry": "US", "addressRegion": "MI" },
  "rating": 5.0,
  "reviewCount": 5,
  "ratingBreakdown": { "itemQuality": 5.0, "shipping": 5.0, "customerService": 5.0, "buyersRecommend": 100.0 },
  "reviews": [
    {
      "reviewId": "5150597893",
      "author": "exampleBuyer",
      "rating": 5,
      "date": "Jul 27, 2026",
      "body": "Great quality, exactly as described!",
      "photos": [],
      "helpfulCount": null,
      "variantContext": null
    }
  ],
  "variations": [
    { "name": "Primary color", "options": [{ "variationId": "6788604158", "label": "citron/blue" }] }
  ],
  "url": "https://www.etsy.com/listing/4530068104/example-listing-slug",
  "searchMode": "search",
  "scrapedAt": "2026-01-01T00:00:00Z"
}
```

**Was-price & sale detection.** `originalPrice`/`discountPercentage`/`onSale`
reflect a seller's own sale badge against their own prior price on that same
listing — never a comparison against a different listing. Full-price listings
carry `originalPrice: null`.

**Reviews are item-level.** They come from the listing's own page, not a
shop-wide review feed. A listing with no reviews returns `reviews: []` and
`rating`/`reviewCount` as `null`. Some review fields (`helpfulCount`,
`variantContext`) are included for completeness but are not exposed on every
listing — they come back `null` when the listing's page doesn't carry them.

**Incremental mode only.** When `incrementalMode` is on, every returned record
also carries:

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

### Resume & recurring updates

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

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

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

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

Scheduled-run example — same search, run daily:

Day 1 (first run ever for this search):

```json
{ "mode": "search", "queries": ["handmade necklace"], "incrementalMode": true }
```

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

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

```json
{ "mode": "search", "queries": ["handmade necklace"], "incrementalMode": true }
```

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

### Proxy & connection

Apify Proxy is recommended for reliable results; a residential connection is
the default. On the Free plan (no residential proxy access) results may be
less reliable — enable Apify Proxy or upgrade your plan at
<https://apify.com/pricing>.

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

Optionally pipe results into the apps you already use through Model Context
Protocol (MCP) connectors. Authorize a connector once under Apify, Settings,
Integrations, then select it in the `mcpConnectors` field. Each connector
receives a condensed, human-readable summary per listing (title plus key
fields), while the complete record always stays in the Apify dataset. For
Notion, set `notionParentPageUrl` to the page the item pages should be created
under. Supported connectors: Notion, Linear, Airtable, and Apify. Leave the
field empty to skip; it never changes the dataset output.

### Notes

- Search and category result pages are walked forward one page at a time until
  *Max listings* is reached, the site runs out of pages, or (with the default
  unlimited *Max pages*) a page repeats no new listings.
- Long runs survive an Apify server migration or a manual Resurrect: progress
  is checkpointed, so a resumed run continues without re-scraping or
  double-charging listings already collected.
- `searchMode` on each record reports how it was found: `search`, `browse`
  (a pasted category/search/shop link), or `listing` when it came from a
  pasted listing link or id.
- Prices are in **USD**.
- Duplicate listings (same id) are returned once per run.

# Actor input Schema

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

Choose 'search' for keywords + filters, or 'url' to scrape pasted Etsy links directly — listing links, category/search links and shop links are all accepted.

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

One or more keywords to search, for example 'handmade necklace' or 'vintage lamp'. Each keyword is searched separately.

## `categoryPath` (type: `string`):

Only used when mode = search. Optional — an Etsy category path to scope the search or browse to, for example 'jewelry/necklaces'. To get one: open etsy.com, browse to the category, and copy the part of the address after '/c/', for example https://www.etsy.com/c/jewelry/necklaces → 'jewelry/necklaces'.

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

Only used when mode = url — ignored in every other mode. Mix freely: listing links (https://www.etsy.com/listing/<id>/<slug>), bare listing ids, category/search links (https://www.etsy.com/c/... or https://www.etsy.com/search?q=...), or shop links (https://www.etsy.com/shop/<name>). Multiple entries supported.

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

How Etsy orders the results.

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

Optional. Keep only listings currently on sale (Etsy's own 'On sale' filter).

## `freeShippingOnly` (type: `boolean`):

Optional. Keep only listings with free shipping (Etsy's own 'Free shipping' filter).

## `personalizableOnly` (type: `boolean`):

Optional. Keep only listings that can be personalized (Etsy's own 'Personalizable' filter).

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

Optional. Only keep listings priced at or above this amount.

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

Optional. Only keep listings priced at or below this amount.

## `shipToCountry` (type: `string`):

Optional. Two-letter country code to only show listings that ship to that country, for example 'US', 'AU', 'FR'. Leave empty for Etsy's default.

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

Collect each listing's full detail: description, materials, category breadcrumbs, extra images, shipping origin, variations, and — when present on the listing — its rating breakdown and item-level reviews (author, rating, date, body, photos). Turn off for a faster, lighter run that returns only the search-card fields (title, shop, price, was-price, image).

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

Maximum number of listings to return across the whole run. This is the run's cap. Use 0 for unlimited.

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

Maximum result pages walked per keyword / category / pasted link. 0 = unlimited (walk all pages) — the run then stops only at Max listings or when a page repeats no new listings.

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

Paste a previous run ID or dataset ID to continue a large crawl of listings without returning or charging for listings already collected there. Use this after an interrupted run. For recurring daily monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring. The first run returns all matching listings as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED listings. Turn on "Emit unchanged" or "Emit expired" only when you also want those listings returned (and billed). State is kept separately for each search/URL and detail-mode 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 search/URL and detail settings — different searches then never mix state with each other.

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

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

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

Apify Proxy is required for reliable results. The actor connects through a residential exit by default.

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

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

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

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

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

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

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "handmade necklace"
  ],
  "urls": [
    "https://www.etsy.com/search?q=handmade+necklace",
    "https://www.etsy.com/listing/4530068104/monogram-punch-needle-bag-charm"
  ],
  "sortBy": "most_relevant",
  "onSaleOnly": false,
  "freeShippingOnly": false,
  "personalizableOnly": false,
  "fetchDetails": true,
  "maxItems": 20,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

# API

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

## JavaScript example

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

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

// Prepare Actor input
const input = {
    "mode": "search",
    "queries": [
        "handmade necklace"
    ],
    "urls": [
        "https://www.etsy.com/search?q=handmade+necklace",
        "https://www.etsy.com/listing/4530068104/monogram-punch-needle-bag-charm"
    ],
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/etsy-marketplace-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "search",
    "queries": ["handmade necklace"],
    "urls": [
        "https://www.etsy.com/search?q=handmade+necklace",
        "https://www.etsy.com/listing/4530068104/monogram-punch-needle-bag-charm",
    ],
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/etsy-marketplace-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",
  "queries": [
    "handmade necklace"
  ],
  "urls": [
    "https://www.etsy.com/search?q=handmade+necklace",
    "https://www.etsy.com/listing/4530068104/monogram-punch-needle-bag-charm"
  ],
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call abotapi/etsy-marketplace-scraper --silent --output-dataset

```

## MCP server setup

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