DEV Community

publicrecords
publicrecords

Posted on

Track Etsy Shop Sales Velocity with an Apify Actor (curl + Python)

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"]
  }'
Enter fullscreen mode Exit fullscreen mode

Useful input fields (from the Store input schema):

  • shops — array of shop names (the path after etsy.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}'
Enter fullscreen mode Exit fullscreen mode

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

  1. sales_count + as_of — an exact public counter at a known date. Treat it as a measurement, not an Etsy Analytics export.
  2. sales_per_day — derived from consecutive exact reads of the same shop. If history is short, the rate is noisier.
  3. 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.
  4. Compare shops side-by-side on sales_per_day and delta_7d, not on lifetime sales_count alone (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"),
    )
Enter fullscreen mode Exit fullscreen mode

Where this fits

  • Competitor shortlists: pin 10–50 shops, schedule weekly, watch delta_7d.
  • Niche scans: pass keywords / category instead 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)