I run a visa agency in Bali. The other half of my week goes into Apify Actors, and one of them profiles Shopify stores: you hand it a list of domains and it returns one flat row per shop — who the merchant is, the e-mails and socials they published, the theme and apps the storefront runs, catalogue size, price band.
Nobody buys that row for the price band. They buy it for one question that decides whether a store is worth a pitch, an outreach e-mail or a line in a market sizing: is anyone still working on this shop?
Shopify answers that question in public, for free, on every store that hasn't switched it off. It just answers it with two fields that look interchangeable and are not.
The measurement
On 17 October 2026 I read /products.json from four Shopify stores — no login, no API key, no proxy, the same JSON the storefront downloads for itself — and counted products two ways: how many were created in the last 90 days, and how many were published in the last 90 days.
| Store | Sample | Created in 90 days | Published in 90 days |
|---|---|---|---|
| allbirds.com | 692 | 0 | 404 |
| ridge.com | 750 | 157 | 295 |
| deathwishcoffee.com | 147 (whole catalogue) | 17 | 37 |
| kith.com | 750 | 410 | 500 |
Every store reports more recent publishing than recent creation. That gap is the whole post. On a genuinely busy shop it's roughly a doubling and both numbers point the same way. On Allbirds the two fields disagree about whether the catalogue has moved at all this quarter: one says 404 products, the other says none.
Sort the Allbirds sample by published_at and the reason shows up immediately:
top published_at days: 2026-09-25 → 398 products
2026-06-16 → 222
2026-06-11 → 50
top created_at days: 2025-08-06 → 116
2025-07-23 → 44
2025-07-03 → 20
created_at by year: 2018: 5 · 2019: 7 · 2020: 11 · 2021: 28 · 2022: 35
2023: 85 · 2024: 154 · 2025: 340 · 2026: 27
398 of the 692 products in that sample carry a published_at inside one afternoon — the earliest at 14:51:05, the latest at 16:58:13 on 25 September 2026. Two hours and seven minutes for 58% of a catalogue that took eight years to build. Here is one of them:
Men's Canvas Runner NZ - Deep Navy Stripes
created_at: 2025-12-04T12:15:25-08:00
published_at: 2026-09-25T16:58:09-07:00
updated_at: 2026-10-17T14:08:56-07:00
A product that has existed for ten months, stamped as published three weeks ago. The newest created_at anywhere in those 692 products is 2026-06-05 — four months before the run. The oldest is 2018-11-12.
I can't see from outside which admin operation did that: a sales-channel change, a market or theme migration, a bulk edit, a re-publish of unpublished seasonal lines. It doesn't matter for the consumer of the data. What matters is that published_at is not a creation timestamp and nothing in the payload says so. An activity metric built on it reports a store that added nothing in a quarter as the most active shop in your list.
updated_at is worse
The other candidate is the one most people reach for second. Don't:
allbirds.com 250 of 250 products · updated_at = 2026-10-17
deathwishcoffee.com 147 of 147 products · updated_at = 2026-10-17
Every product in both catalogues, 100% of each, updated today. Inventory sync, price feeds and app writes touch the record constantly, so updated_at is a heartbeat of Shopify's own plumbing, not of the merchant. As a freshness signal it has no variance at all, which makes it exactly as informative as a constant.
What the Actor counts instead
Nine lines, and the comment is the whole argument:
const now = Date.now();
// Count on created_at, not published_at: Shopify bumps published_at whenever a product is
// re-published, so a store that changed nothing all year still looks brand new.
const fresh = products.filter(
(p) => p.created_at && (now - Date.parse(p.created_at)) < 90 * 864e5
).length;
created_at is the field that only moves once. It survives re-publishing, channel changes and theme migrations, because none of those create a product record — they touch one that already exists.
That lands in the store row as three fields: productsCreatedLast90Days, newestProductAt and oldestProductAt. Here is the real Allbirds row from a run of the published Actor on 17 October, trimmed to the parts under discussion:
{
"rowType": "store",
"shopName": "Allbirds",
"myshopifyDomain": "weareallbirds.myshopify.com",
"email": "help@allbirds.com",
"emails": ["help@allbirds.com", "travis@allbirds.com", "press@allbirds.com", "info@afterpay.com"],
"theme": "[DNAM Theme July 2026]",
"productCount": 250,
"totalProducts": 692,
"catalogueComplete": false,
"variantCount": 2720,
"inStockVariants": 495,
"outOfStockVariants": 2225,
"priceMin": 4, "priceMedian": 80, "priceMax": 165,
"newestProductAt": "2026-05-26T11:10:21-07:00",
"oldestProductAt": "2018-11-12T14:25:43-08:00",
"productsCreatedLast90Days": 0,
"pageCount": 175,
"firstPageAt": "2017-04-25T14:22:24-07:00"
}
Three other numbers in that row point the same direction as the zero: 2,225 of 2,720 variants out of stock, and a catalogue whose oldest content page dates to April 2017. That's a store with eight years of history and nothing new on the shelf this quarter — which is a perfectly good thing to know before you spend a sales hour on it, and the exact opposite of what published_at would have told you.
Two caveats, because this is not a law of nature
created_at lies about replatforming. It records when the record was made, not when the product existed. Kith shows 665 of 750 sampled products created in 2026 and 1 in 2018. Some of that is a genuinely fast fashion catalogue; some of it is what happens when a catalogue is rebuilt or bulk-imported, because every imported product gets a brand-new created_at. A store that migrated last month looks newborn on this metric. There is no public field that survives that, and I'm not going to pretend otherwise — the honest read is "this record is young", not "this business is young".
The sample is not the catalogue. /products.json pages out at whatever cap you set — 250 by default. Allbirds returned productCount: 250 against totalProducts: 692, counted separately from the store's product sitemap, with catalogueComplete: false saying so out loud. Kith came back 250 against 34,970. Every catalogue figure in the row, including the 90-day count, describes the sample; the Actor's own newestProductAt for Allbirds reads 2026-05-26 from its 250-product sample against 2026-06-05 across all 692. If the activity number is what you're buying, raise the per-store product cap and read catalogueComplete before you trust it.
The fourth store
One of the four addresses in that run never produced a profile:
{
"rowType": "error",
"storeUrl": "https://ridge.com",
"error": "The shop refused the request (HTTP 403), so it could not be checked. Slow the run down with fewer parallel stores, or try again later.",
"scrapedAt": "2026-10-17T21:09:32.400Z"
}
ridge.com rate-limited the Actor during the run, having answered a plain request from the same machine a few minutes earlier — that's the 157/295 row in the table above. The thing I care about is that it comes back as refused, not as isShopify: false. A scraper that reports a 403 as "not a Shopify store" silently deletes real shops from a prospect list and gives you no way to notice. Error rows cost nothing and one bad address doesn't stop the run.
Calling it
One HTTP call, rows back in the same request:
curl -X POST "https://api.apify.com/v2/acts/lergassy~shopify-store-intel/run-sync-get-dataset-items?token=<TOKEN>" \
-H "Content-Type: application/json" \
-d '{"storeUrls":["allbirds.com","deathwishcoffee.com"],"mode":"store","maxProductsPerStore":250}'
Four addresses in store mode took 22 seconds end to end. The Actor is shopify-store-intel; the dates, the 403 and the Allbirds row above all come from that run.
When not to bother
If you only need to know whether a domain is Shopify, don't profile it — detect mode answers that from one request. If you need stock levels or sales volume, this is the wrong data source entirely: /products.json publishes an available boolean per variant and no quantities, and nothing public tells you what sold. And if your list is stores you already know are active, the activity fields are dead weight — buy the contacts and skip the catalogue.
But if the list is 5,000 domains scraped off a directory and your real question is which 400 are worth a human reading them, count creations. Counting publications tells you when somebody last clicked Save.
Top comments (0)