~ / guides / How to Scrape Etsy (Python Guide)

How to Scrape Etsy (Python Guide)

MR
Marcus Reed
Founder & lead tester · about the author
the short version
  • A plain requests + BeautifulSoup scraper returns HTTP 403 on Etsy search and category pages, served by DataDome. I reproduced this on two URL types in June 2026.
  • Etsy answers with a ~795-byte JS challenge titled etsy.com ("Please enable JS and disable any ad blocker") instead of listings, so a static scraper has nothing to parse.
  • Etsy's robots.txt returns 200 (it sits outside the bot wall) and disallows /api/, /search?*q=, and */shop/*/sold* among 1,758 lines of rules.
  • Two routes return data: Etsy's official Open API v3 (free key, OAuth, sanctioned) for catalog access, or a scraper API like ChocoData when you need the rendered public page.

Etsy is a rich scrape target: handmade and vintage listings, prices, shop ratings, review text, tags, and sold counts across millions of small sellers. It is also wrapped in a commercial bot wall. This guide builds a real Python scraper, runs it against live Etsy pages, and shows exactly where it breaks. I ran the code in June 2026 and pasted the real output below, including the 403 that stopped it. Then I cover the two routes that actually return data: Etsy’s own API and a scraper API.

You can request a public Etsy page with a script, but Etsy blocks static scrapers at the door and its terms restrict automated access. Two legal layers matter, and they sit separately from the technical wall (covered below, which stops a naive scraper before any of this is tested in court).

On US computer-access law, reading pages anyone can load without logging in is the safer side of the line. In hiQ Labs v. LinkedIn, the Ninth Circuit held that scraping public profiles does not violate the Computer Fraud and Abuse Act, and the Supreme Court’s Van Buren decision narrowed the CFAA toward traditional hacking. Fetching a public listing page with a bot is, on its own, not a federal computer crime.

Contract law is the tighter constraint for Etsy. Etsy’s Terms of Use bind every visitor, and Etsy directs developers to its API Terms of Use for any programmatic access. Etsy also handles a large volume of buyer and seller personal data, so scraping names, locations, or review authors pulls in the GDPR in Europe and the CCPA in California. The safe posture: stay on public listing and shop pages, never script a logged-in account, avoid collecting personal data, and prefer the Open API for anything commercial. I am a tester, not a lawyer, so get advice before a commercial scrape. My is web scraping legal pillar covers the full picture.

What does Etsy’s robots.txt say?

Etsy’s robots.txt returns HTTP 200 and runs to 1,758 lines, disallowing the API path, keyword-search URLs, and sold-listing paths. It sits outside the DataDome wall, so a script reads it fine even though it cannot read the listing pages. I fetched it live in June 2026 (51,341 bytes). The block that applies to a generic scraper is User-agent: *:

User-agent: *
Disallow: /api/
Disallow: /your/
Disallow: /transactions/
Disallow: /cart
Disallow: /conversations/
...
Disallow: /search?*q=
Disallow: /search/?*q=
Disallow: */shop/*/sold*
Disallow: */listing/*/favoriters*

Three things stand out. The keyword-search URLs you would most want (/search?*q=) are disallowed. The sold-listing view (*/shop/*/sold*) is off limits, so historical sales data is explicitly fenced. And there is no Crawl-delay directive under * and no named entry for AI crawlers like GPTBot or ClaudeBot in the file as fetched, though Etsy gives specific bots (such as AdsBot-Google-Mobile) their own carve-outs. robots.txt is not law, but ignoring it is evidence of intent in a dispute and a breach of the site terms that reference it. The practical point is in the next section: even if you ignore the file, DataDome blocks the request anyway.

What data is available on an Etsy listing or search page?

A search results page and a listing page together carry most of the commercially useful fields. Here is what sits in the public HTML and the typical selector, assuming you can retrieve the page (the next section shows why a static scraper cannot):

FieldWhereTypical selector
Listing titleSearch + listing.v2-listing-card__title / h1[data-listing-id]
PriceSearch + listing.currency-value / [data-buy-box-region="price"]
CurrencySearch + listing.currency-symbol
Shop nameSearch + listing.v2-listing-card__shop / a[href*="/shop/"]
Star ratingListinginput[name="rating"] / .stars-svg aria-label
Review countListing.wt-text-body-01 a[href*="#reviews"]
Review textListing.review-list .wt-content-toggle
Listing IDURL/listing/{id}/...
Tags / categoriesListinga[href*="/market/"] chips
Image URLsSearch + listingimg in .listing-link / carousel

The descriptive fields and the price sit in the static HTML, so parsing is not the hard part on Etsy. Retrieval is. The challenge is getting any 200 response at all from a script, because DataDome answers first.

Method 1: scrape Etsy with requests and BeautifulSoup (what really happened)

A requests GET with a real browser User-Agent plus BeautifulSoup is the standard first attempt, and on Etsy it fails at the request. Here is the exact code I ran against a live Etsy search page:

import requests
from bs4 import BeautifulSoup

HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/124.0.0.0 Safari/537.36"
    ),
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Sec-Fetch-Mode": "navigate",
}

url = "https://www.etsy.com/search?q=leather+wallet"
resp = requests.get(url, headers=HEADERS, timeout=25)
print("STATUS:", resp.status_code)
print("BYTES :", len(resp.content))
print("SERVER:", resp.headers.get("server"))

soup = BeautifulSoup(resp.text, "html.parser")
title = soup.find("title")
print("TITLE :", title.get_text(strip=True) if title else None)

cards = soup.select(".v2-listing-card, [data-listing-id]")
print("CARDS :", len(cards))

This is the real output I got, run on a search URL and confirmed again on a /c/ category URL:

STATUS: 403
BYTES : 796
SERVER: DataDome
TITLE : etsy.com
CARDS : 0

Etsy returned a 403 with a ~795-byte page from the DataDome server instead of listings, so CARDS is 0 because there is nothing to parse. The body is a JavaScript challenge that reads “Please enable JS and disable any ad blocker” and carries a DataDome cid/hsh token, not product HTML. I sent a full browser header set and still got blocked, and I reproduced the same 403 on a category page (/c/jewelry). I did not find a cookie-less requests path that returns the real page, so I am not pasting any parsed listing fields. There were none to paste.

How Etsy blocks you

Etsy fronts its pages with DataDome, which scores the whole connection rather than the User-Agent string. The block comes from a layered detection stack:

Matching the User-Agent solves none of these. To get past them with DIY tooling you would need a real browser engine (Playwright) that executes the DataDome JS, plus rotating residential IPs so the IP reputation check passes. That is most of what a scraper API already bundles. Before going down the proxy rabbit hole, see how to scrape without getting blocked.

Method 2: the official Etsy Open API v3

For catalog, shop, and review data, Etsy’s own API is the sanctioned route and the personal key is free. The Open API v3 returns listings, shops, and reviews as JSON. You register an app at developer.etsy.com, get an API key (the x-api-key), and most read endpoints then use an OAuth 2.0 token. The request shape looks like this:

import requests

API_KEY = "YOUR_ETSY_KEYSTRING"   # personal key from developer.etsy.com
TOKEN   = "YOUR_OAUTH_TOKEN"      # OAuth 2.0 access token

endpoint = "https://openapi.etsy.com/v3/application/listings/active"
headers = {
    "x-api-key": API_KEY,
    "Authorization": f"Bearer {TOKEN}",
}
params = {"keywords": "leather wallet", "limit": 5}

resp = requests.get(endpoint, headers=headers, params=params, timeout=30)
print("STATUS:", resp.status_code)
# resp.json()["results"] -> listing_id, title, price, tags, shop info, ...

The API gives you clean, structured fields without parsing HTML or fighting DataDome, and it is the lowest-legal-risk option because you are using Etsy the way Etsy sanctions. The trade-offs: you must accept the API Terms of Use, tokens expire and need refreshing, calls are rate-limited (Etsy documents per-second and per-day quotas), and some endpoints require app approval beyond the personal key. I did not paste live API output here because that needs a registered key and OAuth token, and I will not fake numbers. The endpoint and headers above come from Etsy’s developer docs.

Method 3: scrape Etsy with a scraper API

When you need the rendered public page itself (a listing’s review section, a shop page, fields the Open API does not expose), a scraper API gets past the 403 that stopped Method 1. It runs the request through a real browser engine that executes the DataDome JavaScript and a rotating residential IP, so Etsy sees a browser-shaped connection instead of a Python script.

ChocoData is the service I use for this category. It exposes a universal endpoint (one URL for any site) plus 453 dedicated endpoints for common targets. You hand it the Etsy URL and it returns the rendered page. Here is the request shape, swapping the raw requests.get from Method 1 for the API:

import requests

API_KEY = "cd_live_YOUR_KEY"   # free tier: 1,000 requests/month, no card
target = "https://www.etsy.com/search?q=leather+wallet"

endpoint = "https://api.chocodata.com/api/v1/universal/get"
params = {
    "api_key": API_KEY,
    "url": target,
    "render": "true",   # run a real browser engine to clear the DataDome JS
    "country": "us",    # US residential IP to match the marketplace
}

resp = requests.get(endpoint, params=params, timeout=90)
print("STATUS:", resp.status_code)
# Rendered HTML comes back in the payload; parse .v2-listing-card__title and
# .currency-value with BeautifulSoup exactly as in Method 1, now with a real page.

The difference from Method 1 is what happens behind that one call. ChocoData fetches through a country-matched residential IP that rotates automatically and retries through a fresh IP when a block appears, and the render flag runs a browser engine that executes the DataDome challenge so the connection clears instead of returning the 403 bare requests got. Per ChocoData’s docs, billing applies only to successful (2xx) responses, so the 403 problem from Method 1 does not quietly drain your budget. I did not paste live API output here because it requires a paid key, and I will not fake numbers. The honest claim is narrow: the universal endpoint takes a URL and returns the rendered page, which removes the specific failure (the DataDome 403) I reproduced in Method 1. I have not independently benchmarked their success rate on Etsy, so treat the parsing claims as vendor-stated until you test on the free tier.

Should you use the API or scrape the page?

Use the official Open API v3 first; reach for a scraper API only for data the official API does not expose or where you need the rendered page. The choice comes down to what you need and how Etsy sanctions it.

ApproachWhat you getCostBlock riskBest for
DIY requests + BS4403 DataDome, no data (my test)FreeBlocked immediatelyNothing on Etsy today
Official Open API v3Structured JSON, sanctionedFree key + rate limitsNone (you are allowed)Listings, shops, reviews
Scraper APIRendered public pageFree tier, then per requestHandled for youReview HTML, shop pages, API gaps

My honest take from the test: a plain Python scraper does not work on Etsy in 2026, because DataDome returns a 403 JS challenge before you reach any listing. If your data is in the Open API v3, use it: it is free, structured, and the route Etsy endorses. If you need the rendered public page beyond what the API gives, a scraper API is the practical way to get a real 200 instead of the 403 I hit.

To go deeper on the building blocks, see the Python web scraping guide for the language fundamentals, BeautifulSoup for parsing, and Scrapy if you are crawling many pages and want a framework. The web scraping pillar ties the whole workflow together.

FAQ

Does Etsy have a free official API for product data?

Yes. The Etsy Open API v3 at developer.etsy.com exposes listings, shops, and reviews as JSON. A personal API key is free; you register an app, the key is approved, and most read endpoints use OAuth 2.0. It is rate-limited (Etsy documents per-second and per-day quotas) and bound by Etsy's API terms, but it is the route Etsy sanctions and it skips the DataDome 403 entirely.

Why does my Etsy scraper get a 403 when the page loads fine in my browser?

Etsy runs DataDome, which fingerprints the whole connection, not just the User-Agent. A real Chrome request carries a specific TLS handshake (JA3/JA4), HTTP/2 frame order, and cookie set that a plain Python requests call cannot reproduce, so DataDome returns a 403 JS challenge to the script while passing the browser. Matching the User-Agent string alone does nothing.

Can I scrape Etsy reviews and sold counts?

Reviews render on the public listing page and the Open API v3 exposes a reviews endpoint, but both sit behind the same wall: the API needs an approved key and the public page returns the DataDome 403 to a static scraper. Sold-listing paths like */shop/*/sold* are also disallowed in robots.txt. A scraper API can fetch the rendered listing page when you need fields the API does not return.

MR
Marcus Reed
I've built and run web scrapers for the better part of a decade. On this site I put scraper APIs and scraping tools through real jobs against real targets, then write up what actually holds up.