Etsy Search Monitor - New Listings, Price & Rank Changes avatar

Etsy Search Monitor - New Listings, Price & Rank Changes

Pricing

from $2.00 / 1,000 change events

Go to Apify Store
Etsy Search Monitor - New Listings, Price & Rank Changes

Etsy Search Monitor - New Listings, Price & Rank Changes

Monitor Etsy searches and categories: new listings, disappeared listings, price changes, rank movement between runs. Persistent per-monitor snapshots. HTTP-only, no browser, no API key.

Pricing

from $2.00 / 1,000 change events

Rating

0.0

(0)

Developer

Heim AI

Heim AI

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

Etsy Search Monitor — New Listings, Price & Rank Changes

Search/category in → change events out. Monitor Etsy market keywords and category pages between runs; get 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 idkaz_kakyo/etsy-search-delta
Minimal input{ "queries": ["ceramic mug"], "monitorId": "default" }
Cost$0.002 / observation + $0.002 / change event
OutputDataset rows: observation, change-event, error, summary

Call it (MCP / API / schedule)

MCP (agents)

{
"actor": "kaz_kakyo/etsy-search-delta",
"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-delta').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.

  1. Save a Task with fixed queries / categoryUrls, a stable monitorId, and your rankMoveThreshold / maxPagesPerTarget.
  2. Schedule the Task (e.g. hourly or daily). Same monitorId every tick — that is what links runs.
  3. Webhook on SUCCEEDED → your endpoint / Zapier / Make. Process type === "change-event" rows; ignore or log error / summary.
  4. Cap spend with maxTotalChargeUsd. When the cap hits, remaining work becomes uncharged type: "error" skipped rows — no surprise bill.
  5. 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

FieldTypeDefaultNotes
queriesstring[]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"].
categoryUrlsstring[]/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.
monitorIdstring"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.
maxPagesPerTargetint 1–51Category ≈ 64 listings/page; market ≈ 8/page.
rankMoveThresholdint ≥15Emit rank-move only when |Δrank| ≥ threshold.
maxObservationsPerRunint 1–50002000Fetch 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 (baselineTargets in 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 maxTotalChargeUsd budget 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 /search backend (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 null on platform runs (US-served cards do not render shop names). Use shopId (numeric string, stable, public) as the reliable shop identity key. Shop name/id are public shop identifiers — no seller personal data.
  • inStock is null when 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 (猫 mug stays 猫 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 monitorId at 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 fresh monitorId to re-baseline at the narrower window.

Pricing

EventPriceWhen
Listing observation$0.002One listing captured in the monitored window this run
Change event$0.002One detected change vs the previous snapshot
Actor startplatform defaultPer 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.