<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Max Prozapas</title>
    <description>The latest articles on DEV Community by Max Prozapas (@mersetti).</description>
    <link>https://dev.to/mersetti</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4184234%2F6b4f6ed7-c0da-4578-90c7-62521f56c702.jpg</url>
      <title>DEV Community: Max Prozapas</title>
      <link>https://dev.to/mersetti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mersetti"/>
    <language>en</language>
    <item>
      <title>Track competitor prices on any Shopify store with products.json (and catch restocks)</title>
      <dc:creator>Max Prozapas</dc:creator>
      <pubDate>Wed, 14 Oct 2026 12:00:02 +0000</pubDate>
      <link>https://dev.to/mersetti/track-competitor-prices-on-any-shopify-store-with-productsjson-and-catch-restocks-39k8</link>
      <guid>https://dev.to/mersetti/track-competitor-prices-on-any-shopify-store-with-productsjson-and-catch-restocks-39k8</guid>
      <description>&lt;p&gt;If a competitor runs on Shopify, their whole catalog — prices, compare-at prices, SKUs, stock flags — is one public&lt;br&gt;
URL away:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://www.allbirds.com/products.json?limit=250&amp;amp;page=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Paging and collections
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;limit&lt;/code&gt; max is 250; increment &lt;code&gt;page&lt;/code&gt; until &lt;code&gt;{"products": []}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;One collection only: &lt;code&gt;/collections/{handle}/products.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Around 25,000 products the deep pages stop — slice big catalogs by collection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The 403 trap
&lt;/h2&gt;

&lt;p&gt;Some big stores block &lt;code&gt;products.json&lt;/code&gt; on their main domain. Their homepage HTML still contains the internal&lt;br&gt;
&lt;code&gt;something.myshopify.com&lt;/code&gt; host — query that one instead and build product URLs on the public domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get per variant
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;id, sku, price, compare_at_price, available, option1..3&lt;/code&gt;, plus product-level &lt;code&gt;title, handle, vendor, product_type,&lt;br&gt;
tags, images, created_at, updated_at&lt;/code&gt;. Discount % = &lt;code&gt;(compare_at − price) / compare_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One caveat: prices are in the store's &lt;strong&gt;default market&lt;/strong&gt; currency and the JSON doesn't say which — don't guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning snapshots into alerts
&lt;/h2&gt;

&lt;p&gt;Store &lt;code&gt;{price, available}&lt;/code&gt; per &lt;code&gt;store:variantId&lt;/code&gt;, then on each run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;new&lt;/td&gt;
&lt;td&gt;not in snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;priceDrop / priceIncrease&lt;/td&gt;
&lt;td&gt;price changed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;backInStock&lt;/td&gt;
&lt;td&gt;available false → true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;outOfStock&lt;/td&gt;
&lt;td&gt;available true → false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;removed&lt;/td&gt;
&lt;td&gt;missing now — only if the store fetch succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 7,000-variant snapshot is ~160 KB gzipped, so you can keep months of history cheaply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference &amp;amp; ready-made tool
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Guide: &lt;strong&gt;&lt;a href="https://github.com/Mersetti/shopify-products-json-guide" rel="noopener noreferrer"&gt;github.com/Mersetti/shopify-products-json-guide&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No-code: &lt;strong&gt;&lt;a href="https://apify.com/mersetti/shopify-products-monitor" rel="noopener noreferrer"&gt;Shopify Products &amp;amp; Price Monitor on Apify&lt;/a&gt;&lt;/strong&gt; —
export catalogs or schedule it daily and get only the changes.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ecommerce</category>
      <category>shopify</category>
      <category>webscraping</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to monitor any company's job openings (Workday, Greenhouse, Lever &amp; 8 more) without a headless browser</title>
      <dc:creator>Max Prozapas</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:35:37 +0000</pubDate>
      <link>https://dev.to/mersetti/how-to-monitor-any-companys-job-openings-workday-greenhouse-lever-8-more-without-a-headless-403g</link>
      <guid>https://dev.to/mersetti/how-to-monitor-any-companys-job-openings-workday-greenhouse-lever-8-more-without-a-headless-403g</guid>
      <description>&lt;p&gt;Want a feed of new openings at a list of companies — for a niche job board, a recruiting pipeline, a hiring-trends dashboard&lt;br&gt;
or just your own job hunt? You don't need Puppeteer. Almost every company publishes jobs through an &lt;strong&gt;applicant tracking&lt;br&gt;
system (ATS)&lt;/strong&gt;, and every major ATS exposes a &lt;strong&gt;public, unauthenticated JSON feed&lt;/strong&gt; for its career pages.&lt;/p&gt;

&lt;p&gt;This post shows the endpoints, the gotchas I hit, and the small amount of code that turns 11 different formats into one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1: find out which ATS a company uses
&lt;/h2&gt;

&lt;p&gt;Open the careers page and look at the links / embedded scripts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You see in the HTML&lt;/th&gt;
&lt;th&gt;ATS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;boards.greenhouse.io/stripe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobs.lever.co/palantir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lever&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobs.ashbyhq.com/openai&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ashby&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Workday&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apply.workable.com/huggingface&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Workable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobs.smartrecruiters.com/BoschGroup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SmartRecruiters&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;…plus Recruitee, Personio, Breezy HR, Teamtailor and Rippling. A regex per pattern over the page HTML detects&lt;br&gt;
most companies automatically.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: call the public feed
&lt;/h2&gt;

&lt;p&gt;The easy ones return everything in one request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Greenhouse (content=true adds descriptions)&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://boards-api.greenhouse.io/v1/boards/stripe/jobs?content=true"&lt;/span&gt;

&lt;span class="c"&gt;# Lever&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://api.lever.co/v0/postings/palantir?mode=json"&lt;/span&gt;

&lt;span class="c"&gt;# Ashby (with structured salary!)&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://api.ashbyhq.com/posting-api/job-board/openai?includeCompensation=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workday is the odd one: a &lt;code&gt;POST&lt;/code&gt; with a JSON body, 20 jobs per page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"limit":20,"offset":0,"searchText":"engineer","appliedFacets":{}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://nvidia.wd5.myworkdayjobs.com/wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tip: use &lt;code&gt;searchText&lt;/code&gt; — paging through 2,000 NVIDIA jobs to find 30 matching ones is slow; Workday's own search isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: the gotchas
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Greenhouse&lt;/strong&gt; descriptions are &lt;em&gt;double-encoded&lt;/em&gt; HTML (&lt;code&gt;&amp;amp;lt;p&amp;amp;gt;&lt;/code&gt;) — decode entities first, then strip tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workday&lt;/strong&gt; dates are text: &lt;code&gt;Posted Today&lt;/code&gt;, &lt;code&gt;Posted 3 Days Ago&lt;/code&gt;, &lt;code&gt;Posted 30+ Days Ago&lt;/code&gt;. The detail endpoint has the
real &lt;code&gt;startDate&lt;/code&gt;. &lt;code&gt;locationsText&lt;/code&gt; may literally be &lt;code&gt;"2 Locations"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workday&lt;/strong&gt; &lt;code&gt;total&lt;/code&gt; is only correct on the first page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recruitee&lt;/strong&gt; dates look like &lt;code&gt;2026-10-13 10:58:56 UTC&lt;/code&gt; — not ISO.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personio&lt;/strong&gt; returns XML, and some companies live on &lt;code&gt;.jobs.personio.com&lt;/code&gt; instead of &lt;code&gt;.de&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SmartRecruiters&lt;/strong&gt; and &lt;strong&gt;Rippling&lt;/strong&gt; need one detail call per job for descriptions (Rippling even for the date).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teamtailor&lt;/strong&gt; usually runs on a custom domain — &lt;code&gt;/jobs.rss&lt;/code&gt; on the careers domain gives you everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: normalize
&lt;/h2&gt;

&lt;p&gt;Map every source into one shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;workplaceType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;employmentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;postedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;salary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;descriptionText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;applyUrl&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Salary deserves a note: only Ashby, Lever, Recruitee and Rippling have structured pay. For the rest, US postings&lt;br&gt;
usually state a range in the text, and one regex catches most of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;([&lt;/span&gt;&lt;span class="sr"&gt;$€£&lt;/span&gt;&lt;span class="se"&gt;])\s?(\d{1,3}(?:&lt;/span&gt;&lt;span class="sr"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\d{3})&lt;/span&gt;&lt;span class="sr"&gt;*|&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)\s?([&lt;/span&gt;&lt;span class="sr"&gt;kK&lt;/span&gt;&lt;span class="se"&gt;])?\s?(?:&lt;/span&gt;&lt;span class="sr"&gt;-|–|to&lt;/span&gt;&lt;span class="se"&gt;)\s?[&lt;/span&gt;&lt;span class="sr"&gt;$€£&lt;/span&gt;&lt;span class="se"&gt;]?\s?(\d{1,3}(?:&lt;/span&gt;&lt;span class="sr"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\d{3})&lt;/span&gt;&lt;span class="sr"&gt;*|&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)\s?([&lt;/span&gt;&lt;span class="sr"&gt;kK&lt;/span&gt;&lt;span class="se"&gt;])?&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reject yearly ranges under 10,000 so "$1,000–$2,000 signing bonus" doesn't become a salary. In my tests this finds&lt;br&gt;
pay for roughly half of all jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: only new jobs
&lt;/h2&gt;

&lt;p&gt;Keep a map of &lt;code&gt;ats:company:jobId → firstSeenAt&lt;/code&gt;. Each run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;job not in the map → &lt;strong&gt;new&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;job in the map but gone now → &lt;strong&gt;closed&lt;/strong&gt;, &lt;em&gt;but only if that company's fetch succeeded&lt;/em&gt; (otherwise one timeout
looks like a mass closure)&lt;/li&gt;
&lt;li&gt;make the very first run a silent baseline, so your "new jobs" feed doesn't start with 3,000 items.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it daily on a schedule and you have a new-jobs feed for any list of companies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full endpoint reference
&lt;/h2&gt;

&lt;p&gt;I documented all 11 endpoints, fields and quirks here:&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/Mersetti/ats-job-board-apis" rel="noopener noreferrer"&gt;github.com/Mersetti/ats-job-board-apis&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't want to maintain it?
&lt;/h2&gt;

&lt;p&gt;I packaged all of the above — 11 ATS, auto-detection from careers URLs, salary extraction, filters and new/closed&lt;br&gt;
monitoring — as an Apify Actor: &lt;strong&gt;&lt;a href="https://apify.com/mersetti/ats-jobs-feed" rel="noopener noreferrer"&gt;Career Sites Jobs Feed&lt;/a&gt;&lt;/strong&gt;. Pay per job,&lt;br&gt;
schedule it, export to CSV/Sheets or call it from an AI agent.&lt;/p&gt;

&lt;p&gt;Which ATS should I add next? Let me know in the comments.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>javascript</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
