Track Etsy Shop Sales Velocity with an Apify Actor (curl + Python)
Disclosure: I maintain this Actor.
If you sell on Etsy (or research niches), you eventually want a simple question answered: how fast is that other shop selling? Etsy's public shop page shows a lifetime sales counter. Read it twice, with dates, and you can derive a measured sales-per-day rate. This tutorial walks through a real owner run of the public Etsy Shop Sales Tracker & Scraper Actor on Apify — not a mock.
What you get back
One dataset row per shop. Useful fields include:
-
shop,shop_url,title,category -
sales_count— the public lifetime sales counter as read -
as_of/snapshot_date— when that counter was read -
sales_per_day— rate derived from consecutive exact reads -
delta_7d,delta_last,history_days - public engagement counters:
reviews_count,rating,admirers,listings_active
The Actor tracks shops it can read on the public page. It is not limited to Etsy Bestsellers, and this post does not claim an "only Bestsellers" scope.
Pricing
from $0.00005 per run start + $0.003 per shop row (free plan, lower on paid plans), as of Oct 27, 2026. Re-check the Store pricing page before budgeting a large crawl.
Real owner run (cited)
| Actor |
publicrecords/etsy-shop-velocity (build 0.2.36) |
| Run ID | cncEDgcntyJbrDaai |
| Dataset ID | O8uLD791UzoylxU6Z |
| Status | SUCCEEDED |
| Started (UTC) | 2026-10-27 14:58:19 |
| Finished (UTC) | 2026-10-27 14:58:23 |
| Input | Store example: {"shops": ["OrelCeramics", "Bestickend"]}
|
| Items | 2 shop rows |
Sample rows from that dataset:
| shop | sales_count | sales_per_day | delta_7d | delta_last | as_of | history_days |
|---|---|---|---|---|---|---|
| OrelCeramics | 111 | 0 | 0 | 0 | 2026-10-26 | 27 |
| Bestickend | 11,475 | 2 | 10 | 6 | 2026-10-26 | 27 |
Bestickend shows a clear weekly change (delta_7d = 10) and a recent step (delta_last = 6) against a dated counter (as_of = 2026-10-26). OrelCeramics is quieter on the same window — useful when you want a side-by-side contrast, not only hot shops.
Call it with curl
Replace APIFY_TOKEN with your Apify API token (Console → Settings → Integrations). Do not commit the real token.
1. Start a run (Store example input)
curl -sS -X POST \
'https://api.apify.com/v2/acts/publicrecords~etsy-shop-velocity/runs?token=APIFY_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"shops": ["OrelCeramics", "Bestickend"]
}'
Useful input fields (from the Store input schema):
-
shops— array of shop names (the path afteretsy.com/shop/) -
keywords— match title / headline -
category— panel category filter -
minSales— lifetime sales floor -
since— ISO date; only shops whose counters changed since then -
maxShops— upper bound on rows returned (and charged)
2. Wait for the run, then read the default dataset
# status (example: the cited run)
curl -sS \
"https://api.apify.com/v2/actor-runs/cncEDgcntyJbrDaai?token=APIFY_TOKEN" \
| jq '{status: .data.status, buildNumber: .data.buildNumber, datasetId: .data.defaultDatasetId}'
# rows (example: the cited dataset)
curl -sS \
"https://api.apify.com/v2/datasets/O8uLD791UzoylxU6Z/items?token=APIFY_TOKEN&clean=1&limit=10" \
| jq '.[] | {shop, sales_count, sales_per_day, delta_7d, delta_last, as_of}'
3. Schedule it
In the Apify Console, open the Actor → Schedules, or create a schedule via the Schedules API pointing at the same Actor. Daily or weekly reads are enough to keep sales_per_day and delta_7d meaningful; denser schedules mostly burn row events.
How to read the numbers
-
sales_count+as_of— an exact public counter at a known date. Treat it as a measurement, not an Etsy Analytics export. -
sales_per_day— derived from consecutive exact reads of the same shop. If history is short, the rate is noisier. -
delta_7d/delta_last— change across roughly a week, and the most recent step, when the history supports it; otherwise check null-reason fields on the row. -
Compare shops side-by-side on
sales_per_dayanddelta_7d, not on lifetimesales_countalone (a huge shop can grow slowly; a small shop can be accelerating).
Minimal Python client
import os, time, requests
TOKEN = os.environ["APIFY_TOKEN"] # never hard-code
ACT = "publicrecords~etsy-shop-velocity"
base = "https://api.apify.com/v2"
r = requests.post(
f"{base}/acts/{ACT}/runs",
params={"token": TOKEN},
json={"shops": ["OrelCeramics", "Bestickend"]},
timeout=60,
)
r.raise_for_status()
run = r.json()["data"]
run_id = run["id"]
while True:
st = requests.get(f"{base}/actor-runs/{run_id}", params={"token": TOKEN}, timeout=60)
st.raise_for_status()
data = st.json()["data"]
if data["status"] in {"SUCCEEDED", "FAILED", "ABORTED", "TIMED-OUT"}:
break
time.sleep(2)
ds = data["defaultDatasetId"]
items = requests.get(
f"{base}/datasets/{ds}/items",
params={"token": TOKEN, "clean": 1},
timeout=60,
).json()
for row in items:
print(
row["shop"],
row.get("sales_count"),
row.get("sales_per_day"),
row.get("delta_7d"),
row.get("as_of"),
)
Where this fits
- Competitor shortlists: pin 10–50 shops, schedule weekly, watch
delta_7d. - Niche scans: pass
keywords/categoryinstead of (or with) explicit shop names. - Free public slice: etsypulse.ca publishes movers/rising from the same family of reads; each shop row there links back to this Actor via "Track this shop's sales".
Limits to say out loud
- Public counters only — not order-level or revenue data.
- A shop can hide sold listings; the lifetime counter may still be readable when listings are not.
- Pricing and field labels can change; re-read the Store page before a large job.
Disclosure: I maintain this Actor.
Store page: https://apify.com/publicrecords/etsy-shop-velocity?ref=devto-etsy-1
Top comments (0)