<?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: rj</title>
    <description>The latest articles on DEV Community by rj (@rjmacarthy).</description>
    <link>https://dev.to/rjmacarthy</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%2F4186852%2F585292a2-39c0-45da-9a57-f8ad7989d4e8.png</url>
      <title>DEV Community: rj</title>
      <link>https://dev.to/rjmacarthy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rjmacarthy"/>
    <language>en</language>
    <item>
      <title>Build a niche job board from company ATS feeds (Greenhouse, Lever, Ashby &amp; Workday)</title>
      <dc:creator>rj</dc:creator>
      <pubDate>Thu, 15 Oct 2026 13:17:25 +0000</pubDate>
      <link>https://dev.to/rjmacarthy/build-a-niche-job-board-from-company-ats-feeds-greenhouse-lever-ashby-workday-2fi1</link>
      <guid>https://dev.to/rjmacarthy/build-a-niche-job-board-from-company-ats-feeds-greenhouse-lever-ashby-workday-2fi1</guid>
      <description>&lt;p&gt;If you're building a niche job board, you don't need to scrape LinkedIn. You probably shouldn't.&lt;/p&gt;

&lt;p&gt;Most companies post jobs on their &lt;strong&gt;applicant tracking system (ATS)&lt;/strong&gt; first, and job boards and aggregators pick them up from there. The four ATSs that cover a large share of startups and big employers (&lt;strong&gt;Greenhouse, Lever, Ashby and Workday&lt;/strong&gt;) all serve their public job boards from JSON endpoints. No login, no API key, no headless browser.&lt;/p&gt;

&lt;p&gt;I run a small crypto job board, &lt;a href="https://cryptogrind.com" rel="noopener noreferrer"&gt;cryptogrind.com&lt;/a&gt;, and this is how it gets its listings. Here's what I learned wiring these up, including the parts that bit me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Greenhouse
&lt;/h2&gt;

&lt;p&gt;Board URLs look like &lt;code&gt;boards.greenhouse.io/figma&lt;/code&gt; or &lt;code&gt;job-boards.greenhouse.io/figma&lt;/code&gt;. The slug (&lt;code&gt;figma&lt;/code&gt;) is all you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://boards-api.greenhouse.io/v1/boards/figma/jobs
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;?content=true&lt;/code&gt; to get descriptions. Watch out: the HTML comes back &lt;strong&gt;entity-encoded&lt;/strong&gt; (&lt;code&gt;&amp;amp;lt;p&amp;amp;gt;&lt;/code&gt;), so decode it before you render or strip tags.&lt;/p&gt;

&lt;p&gt;Useful fields: &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;location.name&lt;/code&gt;, &lt;code&gt;absolute_url&lt;/code&gt;, &lt;code&gt;first_published&lt;/code&gt;, &lt;code&gt;updated_at&lt;/code&gt;, &lt;code&gt;departments&lt;/code&gt;, &lt;code&gt;offices&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lever
&lt;/h2&gt;

&lt;p&gt;Board URLs look like &lt;code&gt;jobs.lever.co/spotify&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://api.lever.co/v0/postings/spotify?mode=json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's paginated with &lt;code&gt;limit&lt;/code&gt; and &lt;code&gt;skip&lt;/code&gt;. Companies on Lever's EU instance are served from &lt;code&gt;api.eu.lever.co&lt;/code&gt; instead, and their boards live on &lt;code&gt;jobs.eu.lever.co&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Useful fields: &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt; (the title), &lt;code&gt;categories.location&lt;/code&gt;, &lt;code&gt;categories.team&lt;/code&gt;, &lt;code&gt;categories.commitment&lt;/code&gt;, &lt;code&gt;workplaceType&lt;/code&gt;, &lt;code&gt;hostedUrl&lt;/code&gt;, &lt;code&gt;createdAt&lt;/code&gt; (milliseconds since epoch), and sometimes a structured &lt;code&gt;salaryRange&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ashby
&lt;/h2&gt;

&lt;p&gt;Board URLs look like &lt;code&gt;jobs.ashbyhq.com/linear&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://api.ashbyhq.com/posting-api/job-board/linear?includeCompensation=true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ashby is the nicest of the four: &lt;code&gt;isRemote&lt;/code&gt;, &lt;code&gt;workplaceType&lt;/code&gt;, &lt;code&gt;employmentType&lt;/code&gt;, &lt;code&gt;publishedAt&lt;/code&gt;, plain-text descriptions, and real compensation data.&lt;/p&gt;

&lt;p&gt;One gotcha that cost me: salary lives in &lt;code&gt;compensation.summaryComponents[]&lt;/code&gt;, not in a &lt;code&gt;minValue&lt;/code&gt; / &lt;code&gt;maxValue&lt;/code&gt; pair on the compensation object. My first version read the wrong path and silently got no salaries at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workday
&lt;/h2&gt;

&lt;p&gt;Workday is where the big employers are, and it's the awkward one. Career sites look like &lt;code&gt;nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite&lt;/code&gt;. The tenant (&lt;code&gt;nvidia&lt;/code&gt;), the &lt;code&gt;wdN&lt;/code&gt; host and the site name (&lt;code&gt;NVIDIAExternalCareerSite&lt;/code&gt;) all go into the URL:&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://nvidia.wd5.myworkdayjobs.com/wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;appliedFacets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;searchText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jobPostings&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;limit&lt;/code&gt; maxes out at 20&lt;/strong&gt;, so you page with &lt;code&gt;offset&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;total&lt;/code&gt; only comes back on the first page.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every search is capped at 2,000 results.&lt;/strong&gt; NVIDIA reports &lt;code&gt;total: 2000&lt;/code&gt; but actually has over 2,700 open roles. To get them all, split the search by facet (job category or location, both returned in the response) and merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dates are text&lt;/strong&gt;: &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 real start date is only in the job detail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descriptions need a second request per job&lt;/strong&gt; (GET the &lt;code&gt;externalPath&lt;/code&gt; under the same &lt;code&gt;/wday/cxs/...&lt;/code&gt; prefix), and Workday rate-limits each site to a few requests per second after an initial burst. Back off on 429s.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A minimal multi-ATS fetcher
&lt;/h2&gt;

&lt;p&gt;Here's the core of it for the three simple ATSs, normalized into one shape. Node 18+ has &lt;code&gt;fetch&lt;/code&gt; built in:&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;sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;greenhouse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://boards-api.greenhouse.io/v1/boards/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/jobs`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`greenhouse:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;j&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="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;j&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;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;absolute_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;postedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first_published&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;lever&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.lever.co/v0/postings/&lt;/span&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="s2"&gt;?mode=json`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;postings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;postings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`lever:&lt;/span&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="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&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;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;postedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;ashby&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`https://api.ashbyhq.com/posting-api/job-board/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?includeCompensation=true`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ashby:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;board&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;j&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="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;postedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publishedAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;boards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;greenhouse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;figma&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lever&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;spotify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ashby&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linear&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;boards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&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;slug&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sources&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;slug&lt;/span&gt;&lt;span class="p"&gt;)))).&lt;/span&gt;&lt;span class="nf"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jobs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 255 when I ran it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gets you a working feed in an afternoon. The real work is everything after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that take longer than you think
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Finding which ATS a company uses.&lt;/strong&gt; Most careers pages link or embed their ATS board, so you can fetch the page and look for &lt;code&gt;greenhouse.io&lt;/code&gt;, &lt;code&gt;lever.co&lt;/code&gt;, &lt;code&gt;ashbyhq.com&lt;/code&gt; or &lt;code&gt;myworkdayjobs.com&lt;/code&gt; links. Pages that load jobs with JavaScript won't show them in the raw HTML, and some companies use none of the four.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normalizing.&lt;/strong&gt; Every ATS says "remote" differently: a flag, a &lt;code&gt;workplaceType&lt;/code&gt;, or just the word in the location string. Employment types, locations and dates all differ too. Pick one schema early and map everything into it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salaries.&lt;/strong&gt; Ashby and some Lever boards give structured pay. Greenhouse mostly doesn't, so you parse ranges like &lt;code&gt;$134.4K – $215K&lt;/code&gt; or &lt;code&gt;£50,000–£65,000 per year&lt;/code&gt; out of the description text. US pay-transparency laws mean more listings include a range, which makes this worth doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only new jobs.&lt;/strong&gt; For a job board you want each run to return only jobs you haven't seen. Store the IDs you've seen per board, and only mark a job as seen &lt;em&gt;after&lt;/em&gt; you've actually saved it. Otherwise a crashed run loses jobs forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being polite.&lt;/strong&gt; These are public endpoints, but they're not yours. Keep concurrency low, cache, and back off on 429s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or skip all of that
&lt;/h2&gt;

&lt;p&gt;I packaged all of this, including the Workday splitting, salary parsing, ATS auto-detection and the "only new jobs" state, into two Apify Actors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/rjmacarthy/ats-jobs-scraper" rel="noopener noreferrer"&gt;ATS Jobs Scraper&lt;/a&gt;&lt;/strong&gt;: paste Greenhouse, Lever, Ashby or Workday board URLs, or just careers pages, and get one normalized schema back. Turn on &lt;strong&gt;Only new jobs&lt;/strong&gt; and schedule it daily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://apify.com/rjmacarthy/workday-jobs-scraper" rel="noopener noreferrer"&gt;Workday Jobs Scraper&lt;/a&gt;&lt;/strong&gt;: built just for Workday. It gets past the 2,000-result cap automatically and includes full descriptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both cost &lt;strong&gt;$1.50 per 1,000 jobs&lt;/strong&gt;, with no browser or proxy needed. Jobs filtered out or already seen cost nothing. You can call them straight from your backend:&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="s2"&gt;"https://api.apify.com/v2/acts/rjmacarthy~ats-jobs-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &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;'{"companies":["https://boards.greenhouse.io/stripe","https://jobs.ashbyhq.com/ramp"],"onlyNewJobs":true,"stateKey":"my-board"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or schedule a run in Apify and point a webhook at your API to insert new jobs as they appear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Niche board ideas
&lt;/h2&gt;

&lt;p&gt;A board is only as good as the filter it applies to everyone else's data. A few that are underserved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Roles that offer &lt;strong&gt;visa sponsorship&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Europe-only remote&lt;/strong&gt; roles, as opposed to "remote, but US only"&lt;/li&gt;
&lt;li&gt;Jobs at &lt;strong&gt;AI startups&lt;/strong&gt;. A lot of them are on Ashby.&lt;/li&gt;
&lt;li&gt;One stack or one industry, done properly: Rust, climate, devtools, crypto&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something with this, or need a field or ATS I don't cover yet, leave a comment or open an issue on the Actor. I read every one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm RJ, creator of &lt;a href="https://github.com/twinnydotdev/twinny" rel="noopener noreferrer"&gt;twinny&lt;/a&gt;, an open-source AI coding assistant for VS Code.&lt;/em&gt;&lt;/p&gt;

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