# Etsy Scraper - Listings, Prices and Shops (`s-r/etsy-scraper`) Actor

Scrape Etsy category listings: item name, price, currency, availability, seller shop, image and URL. Reads the JSON-LD the category page publishes and pages through automatically.

- **URL**: https://apify.com/s-r/etsy-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** E-commerce, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

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 Scraper

Scrape **Etsy category listings**: item title, price, currency, availability,
the seller's shop name, image and listing URL. Point it at a category and it
pages through automatically.

### Etsy was on our "closed" list, and that was wrong

Worth saying because it may match your own experience. Etsy has a reputation for
refusing scrapers, and a plain request often does get a 403.

eight
consecutive fetches of a category page: **seven returned about 1.07 MB carrying
a clean JSON-LD `ItemList` of 56 to 62 listings.** One returned a 777-byte 403.

That ratio is the whole story. Etsy refuses roughly **one request in eight**,
and the refusal is not progressive, does not get worse with pacing, and clears
on the next attempt because each retry leaves from a different IP. Retries are
the mechanism here, not damage control. If you see `fetch_failed`, raise
`retries` rather than concluding the site has closed.

### Categories work; search and shops do not

This distinction was measured, six retries per surface, not assumed:

| Surface | Result |
|---|---|
| `/c/jewelry` | 200, 56 listings |
| `/c/jewelry?page=2` | 200, 61 listings |
| `/search?q=leather+bag` | **403 on all six attempts** |
| `/shop/<name>` | **403 on all six attempts** |

A 403 that survives six rotating exits is a wall. A single 403 among eight
successes is luck. They look identical in a log, which is exactly why the
difference has to be measured rather than guessed.

So this Actor **refuses a search or shop URL up front**, with a message telling
you why, instead of spending five retries discovering it for you.

### What comes back

| Field | What it is |
|---|---|
| `listing_id` | Etsy's own id, from the URL. Stable, use it to join runs |
| `name` | Listing title |
| `shop` | The seller's shop |
| `price`, `currency` | Price as a number, plus its currency |
| `availability` | `InStock`, `OutOfStock` and similar |
| `rating`, `rating_count` | When the listing publishes them |
| `image` | Listing image |
| `url` | Listing page, tracking parameters stripped |

A note on **`shop`**: Etsy publishes the seller in the schema.org `brand` field.
Calling that "brand" in the output would imply a manufacturer, which is wrong
for a handmade marketplace where the seller *is* the maker. It is returned as
`shop`, under what it actually means.

A note on **ratings**: category `ItemList` entries frequently carry no rating at
all. Those come back `null` rather than zero, because "not published" and "rated
zero" are different facts and a zero would poison any average you compute.

### Input reference

| Field | Type | Default |
|---|---|---|
| `category_url` | Etsy category page | `https://www.etsy.com/c/jewelry` |
| `limit` | 1-2000 | 120 |
| `retries` | 1-10 | 5 |

Pagination is Etsy's own `?page=N` and stops when a page yields nothing.

### Typical uses

- **Competitor and price research.** A category run gives you the going rate for
  a product type across dozens of sellers in one go. One test run over
  three pages of jewellery returned 120 listings from **94 distinct shops**.
- **Seller discovery.** Group by `shop` to find who is active in a niche and how
  many listings each one is running.
- **Price positioning.** Distribution of `price` within a category is the
  quickest read on where a new listing should sit.
- **Assortment monitoring.** Schedule it and join on `listing_id` to watch
  prices and availability move.

### Notes on behaviour

Pages are paced with a short
randomised gap, which matters more here than on most sites: the refusal rate is
per-IP, and burning through the pool quickly makes it worse for everyone sharing
it.

A page that loads but carries no `ItemList` is reported as `no_listings` rather
than returned as an empty result. A run that looks successful and quietly
returns nothing is the failure mode worth being loud about.

Prices come from the US site in the currency Etsy shows there, which for most
listings is USD but is set by the seller and is returned per row rather than
assumed.

### Finding a category URL

Etsy category URLs are `https://www.etsy.com/c/<category>`, optionally with
sub-paths: `/c/jewelry`, `/c/clothing`, `/c/home-and-living`,
`/c/craft-supplies-and-tools`, `/c/jewelry/rings`. Browse to the category in a
browser and copy the address.

Facet parameters that appear in the URL as you filter (price bands, shipping
options, colour) are passed through unchanged and paginated the same way, so
filtering on Etsy first is usually cheaper than filtering the output afterwards:
the facet is applied before pagination, so you spend fewer requests to get the
rows you actually wanted.

### What this Actor does not do

**No search results.** Covered above: `403` on every exit, six retries each.
This is a hard wall rather than a rate limit, and the Actor refuses those URLs
instead of pretending.

**No shop pages.** Same wall. If you need one seller's full catalogue, the
practical route today is to run the categories they sell in and filter the
output on `shop`.

**No reviews or favourites.** Those live on the listing page, which is a
separate surface this Actor does not fetch. Category `ItemList` entries carry
rating fields only sometimes, and they are returned when present.

**No historical prices.** Each run is a snapshot. `listing_id` is stable, so
scheduling the Actor and joining snapshots on it gives you a price history.

# Actor input Schema

## `category_url` (type: `string`):

An Etsy category page, for example https://www.etsy.com/c/jewelry. Search and shop URLs are refused: Etsy answers 403 to those from every exit tested.

## `limit` (type: `integer`):

How many listings to return. Roughly 58 per page.

## `retries` (type: `integer`):

Etsy refuses about one request in eight regardless of pacing. Each retry uses a fresh exit IP, so retries are how pages get through rather than a sign of trouble.

## Actor input object example

```json
{
  "category_url": "https://www.etsy.com/c/jewelry",
  "limit": 120,
  "retries": 5
}
```

# Actor output Schema

## `listings` (type: `string`):

One row per Etsy listing.

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

Counts, price coverage and how many distinct shops appear.

## `errors` (type: `string`):

Failures with a code and a redacted message.

# 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 = {
    "category_url": "https://www.etsy.com/c/jewelry",
    "limit": 120,
    "retries": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/etsy-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 = {
    "category_url": "https://www.etsy.com/c/jewelry",
    "limit": 120,
    "retries": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/etsy-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 '{
  "category_url": "https://www.etsy.com/c/jewelry",
  "limit": 120,
  "retries": 5
}' |
apify call s-r/etsy-scraper --silent --output-dataset

```

## MCP server setup

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