Etsy Search Scraper — New Listings, Price & Rank Changes
Pricing
from $2.00 / 1,000 change events
Etsy Search Scraper — New Listings, Price & Rank Changes
Scrape Etsy search results for any keyword or category and get only what changed between runs: new listings, disappeared listings, price changes, and rank movement. Persistent per-monitor snapshots. HTTP-only, no browser.
Pricing
from $2.00 / 1,000 change events
Rating
0.0
(0)
Developer
Heim AI
Maintained by CommunityActor stats
1
Bookmarked
1
Total users
0
Monthly active users
23 days ago
Last modified
Categories
Share
Scrape Etsy search results and get only what changed. An Etsy scraper for keywords and category pages that diffs between runs: new listings, disappearances from the watched window, price changes, and rank moves. HTTP-only, no browser. Built for MCP agents, API clients, and scheduled monitors.
| Actor id | kaz_kakyo/etsy-search |
| Minimal input | { "queries": ["ceramic mug"], "monitorId": "default" } |
| Cost | $0.002 / observation + $0.002 / change event |
| Output | Dataset rows: observation, change-event, error, summary |
Call it (MCP / API / schedule)
MCP (agents)
{"actor": "kaz_kakyo/etsy-search","input": {"queries": ["ceramic mug"],"monitorId": "default"}}
Category window (≈64 listings/page):
{"categoryUrls": ["https://www.etsy.com/c/jewelry/necklaces"],"monitorId": "necklaces-daily","maxPagesPerTarget": 2,"rankMoveThreshold": 5}
After the run, read the default dataset. Every row has a type discriminator — filter on "change-event" / "observation"; treat "error" as per-target failure. Bad hosts, empty input, and missing market pages become error rows and the run still SUCCEEDS so agent mistakes do not look like platform outages.
API / apify-client
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('kaz_kakyo/etsy-search').call({ queries: ['ceramic mug'], monitorId: 'default' },{ maxTotalChargeUsd: 1.0 }, // hard budget for this run);const { items } = await client.dataset(run.defaultDatasetId).listItems();const changes = items.filter((i) => i.type === 'change-event');const observations = items.filter((i) => i.type === 'observation');
Same shape via REST: POST /v2/acts/kaz_kakyo~etsy-search-delta/runs with your token, then poll or attach a webhook.
Schedule recipe (the core loop)
This actor is a monitor: one run = one sample of the watched window. Diffs appear only when the same monitorId runs again against the stored snapshot.
- Save a Task with fixed
queries/categoryUrls, a stablemonitorId, and yourrankMoveThreshold/maxPagesPerTarget. - Schedule the Task (e.g. hourly or daily). Same
monitorIdevery tick — that is what links runs. - Webhook on
SUCCEEDED→ your endpoint / Zapier / Make. Processtype === "change-event"rows; ignore or logerror/summary. - Cap spend with
maxTotalChargeUsd. When the cap hits, remaining work becomes unchargedtype: "error"skipped rows — no surprise bill. - First scheduled tick establishes the baseline (observations only, no change events). From the second tick onward you get deltas.
Long runs checkpoint finished targets and aggregates. A platform migration resumes without double-billing: completed targets are skipped, and a migration that hits mid-delivery is resolved in the never-double-bill direction (rows for that target in that run may be missing — an uncharged error row says so). Overlapping runs with the same monitorId are lease-guarded: the second run soft-skips (uncharged) instead of billing the same delta twice.
Input reference
| Field | Type | Default | Notes |
|---|---|---|---|
queries | string[] | — | Keywords → https://www.etsy.com/market/<slug>. Covers Etsy's market page (~8 results/page — top-of-search window), not the full /search backend. Prefill: ["ceramic mug"]. |
categoryUrls | string[] | — | /c/... or /market/... URLs. Locale prefixes (/sg-en/) stripped. https://www.etsy.com/search?q=X is silently converted to the market page for X. Other hosts → uncharged error row. |
monitorId | string | "default" | Snapshot namespace. Display form sanitized to [a-zA-Z0-9-_] (max 60); storage keys also hash the original id, so distinct ids never collide after sanitization. |
maxPagesPerTarget | int 1–5 | 1 | Category ≈ 64 listings/page; market ≈ 8/page. |
rankMoveThreshold | int ≥1 | 5 | Emit rank-move only when |Δrank| ≥ threshold. |
maxObservationsPerRun | int 1–5000 | 2000 | Fetch stops before exceeding. |
At least one of queries / categoryUrls is required at runtime (schema marks both optional so the Console form stays flexible). Max 25 targets per run; overflow → uncharged error rows.
Output contract
Every dataset row includes type. Filter on it.
type: "change-event" (charged)
{"type": "change-event","changeType": "new | disappeared | price-change | rank-move","monitorId": "default","targetKey": "q:ceramic_mug","listingId": "1234567890","title": "…","url": "https://www.etsy.com/listing/1234567890","image": "https://…","shopName": null,"shopId": "21401620","price": 24.0,"currency": "USD","inStock": null,"rank": 3,"oldPrice": 22.0,"newPrice": 24.0,"priceDelta": 2.0,"priceDeltaPct": 9.09,"oldRank": 10,"newRank": 3,"rankDelta": -7}
One listing may emit several change events (e.g. price-change + rank-move). rankDelta negative = improved (moved up). Price fields only on price-change; rank fields only on rank-move.
type: "observation" (charged)
{"type": "observation","monitorId": "default","targetKey": "q:ceramic_mug","listingId": "1234567890","title": "…","url": "https://www.etsy.com/listing/1234567890","image": "https://…","shopName": null,"shopId": "21401620","price": 24.0,"currency": "USD","inStock": null,"rank": 3,"fetchedVia": "datacenter"}
type: "error" (never charged)
{ "type": "error", "monitorId": "default", "targetKey": "…", "url": "…", "error": "…" }
type: "summary" (never charged)
{"type": "summary","monitorId": "default","runId": "…","targets": 1,"targetsProcessed": 1,"observations": 8,"changeEvents": { "new": 0, "disappeared": 0, "price-change": 0, "rank-move": 0 },"baselineTargets": 1,"errors": 0,"chargeLimitReached": { "observation": false, "change-event": false },"residentialFetches": 0,"runAt": "2026-…"}
Delta semantics
- Baseline: first complete fetch per
(monitorId, target)writes a snapshot and emits no change events (baselineTargetsin summary). Incomplete fetches never baseline. - new: listing id present now, absent before.
- disappeared: present before, absent now — means left the monitored window, not necessarily delisted.
- price-change: same listing, same currency, |Δprice| > 0.009. Currency mismatch → skip that listing's price compare (counted in summary). A transiently missing price/title does not erase the previously known value, so recovery never fabricates a change.
- rank-move: |Δrank| ≥
rankMoveThreshold. Rank is 1-based across fetched pages in order. - Coverage guard: change events are emitted only when the window fetch was complete and at least as broad as the previous snapshot. If a page fails, degrades, or coverage shrinks (including lowering
maxPagesPerTarget), the run delivers observations only, keeps the prior snapshot, and explains in an uncharged error row — no false disappearances, ever. - All-or-nothing billing per target: if the remaining
maxTotalChargeUsdbudget cannot cover a target's whole batch, that batch is withheld uncharged (and re-detected next run) rather than partially billed. - Window sizes: category ≈ 64/page; keyword market ≈ 8/page.
Honest limits
- Top-of-window monitoring only — not the full Etsy
/searchbackend (hard-blocked). Keyword monitoring uses/market/<slug>. /search?q=…URLs are auto-converted to market pages.- Currency is whatever the US datacenter response serves (normally USD).
- shopName is typically
nullon platform runs (US-served cards do not render shop names). UseshopId(numeric string, stable, public) as the reliable shop identity key. Shop name/id are public shop identifiers — no seller personal data. - inStock is
nullwhen the served page variant lacks availability data (typical on the platform). Boolean only when Etsy serves rich JSON-LD (some non-US egress). - price is the current (sale) price shown on the card; discounted cards expose sale first, original second — we keep sale.
- Listing-detail fields (description, variants, reviews) are out of scope — SERP/window cards only.
- disappeared ≠ delisted — the listing may have moved below your page window.
- Occasional stray 403s are retried (fresh headers, then optional residential). Cookie jars are never used (a blocked DataDome cookie would poison later requests). Shell/challenge pages (200 HTML with no parseable ItemList) are treated as fetch failures — they can never fabricate change events or overwrite snapshots.
- Nonsense market slugs: true HTTP 404 and an empty first page produce the "no market page" error. Etsy sometimes soft-200s a nonsense slug with a small fallback shelf of real listings; those are monitored as served (they are real listings on that market URL).
- HTTP redirects are followed only to
https://(www.)etsy.com— anything else is blocked (SSRF guard). - Error rows echo user-supplied URLs only after conservative redaction — userinfo removed, every query value and the fragment replaced, and any path segment that follows a credential word (
/reset/…,/token/…) or is an opaque high-entropy blob masked. Credentials or tokens pasted into a URL never reach the dataset, and the same secrets are scrubbed out of free-text error messages. - Keywords keep letters and digits of every script (
猫 mugstays猫 mug); punctuation and symbols are dropped. A keyword is never silently replaced by a subset of itself, so you are never billed for a query you did not ask for. - If a previous run's delivery was interrupted and it cannot be established whether its change events were billed, this actor neither suppresses nor re-bills them: the window is re-detected and its change events are delivered free of charge, with an error row saying so.
- One run per
monitorIdat a time (lease-guarded); overlapping runs soft-skip uncharged. - If your window genuinely shrinks (e.g. you lower
maxPagesPerTarget), the old wider snapshot is kept and change events pause until coverage matches it again — start a freshmonitorIdto re-baseline at the narrower window.
Pricing
| Event | Price | When |
|---|---|---|
| Listing observation | $0.002 | One listing captured in the monitored window this run |
| Change event | $0.002 | One detected change vs the previous snapshot |
| Actor start | platform default | Per run |
Error / summary rows are never billed. Cap spend with maxTotalChargeUsd on the run or task. Charging is atomic charge-on-write: a row is billed exactly when it is written, and any ambiguity (migration or write failure mid-delivery) is always resolved in your favor — never a double charge.
Runs always SUCCEED on bad input — check type: "error" rows.
If this saved you time, a Store review on the actor page helps a solo dev. Hit a problem? Open an issue.