<?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: UDANINN</title>
    <description>The latest articles on DEV Community by UDANINN (@udaninn).</description>
    <link>https://dev.to/udaninn</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%2F4071235%2F16ec38ed-186a-463c-ae92-c18b25a3d8fc.png</url>
      <title>DEV Community: UDANINN</title>
      <link>https://dev.to/udaninn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/udaninn"/>
    <language>en</language>
    <item>
      <title>You can tell which ATS a company uses by looking at its careers URL</title>
      <dc:creator>UDANINN</dc:creator>
      <pubDate>Sat, 15 Aug 2026 14:27:49 +0000</pubDate>
      <link>https://dev.to/udaninn/you-can-tell-which-ats-a-company-uses-by-looking-at-its-careers-url-3i5g</link>
      <guid>https://dev.to/udaninn/you-can-tell-which-ats-a-company-uses-by-looking-at-its-careers-url-3i5g</guid>
      <description>&lt;p&gt;Every job-data project starts with the same question: this company has a careers&lt;br&gt;
page, but what is actually behind it?&lt;/p&gt;

&lt;p&gt;You do not need to guess. Seven of the most common applicant tracking systems put&lt;br&gt;
their identity directly in the URL, and each one has a public JSON endpoint you&lt;br&gt;
can derive from that same URL. Here is the mapping, plus the parts that bite.&lt;/p&gt;
&lt;h2&gt;
  
  
  The lookup table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You see&lt;/th&gt;
&lt;th&gt;ATS&lt;/th&gt;
&lt;th&gt;Public JSON&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;boards.greenhouse.io/acme&lt;/code&gt; or &lt;code&gt;job-boards.greenhouse.io/acme&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boards-api.greenhouse.io/v1/boards/acme/jobs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobs.lever.co/acme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lever&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api.lever.co/v0/postings/acme?mode=json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobs.ashbyhq.com/acme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ashby&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api.ashbyhq.com/posting-api/job-board/acme&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apply.workable.com/acme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Workable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apply.workable.com/api/v1/widget/accounts/acme&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;acme.recruitee.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recruitee&lt;/td&gt;
&lt;td&gt;&lt;code&gt;acme.recruitee.com/api/offers/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;careers.smartrecruiters.com/acme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SmartRecruiters&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api.smartrecruiters.com/v1/companies/acme/postings&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;acme.wd5.myworkdayjobs.com/Careers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Workday&lt;/td&gt;
&lt;td&gt;see below - this one is different&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you would rather not maintain the detection logic and the per-platform&lt;br&gt;
quirks below, it is packaged as a hosted scraper that does the detection for you:&lt;br&gt;
&lt;a href="https://apify.com/practical_ophthalmologist_iuq/career-page-job-monitor" rel="noopener noreferrer"&gt;ATS Jobs &amp;amp; Hiring Signals&lt;/a&gt;, callable from&lt;br&gt;
&lt;a href="https://apify.com/practical_ophthalmologist_iuq/career-page-job-monitor/api/python" rel="noopener noreferrer"&gt;Python&lt;/a&gt; or &lt;a href="https://apify.com/practical_ophthalmologist_iuq/career-page-job-monitor/api/javascript" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Six of the seven are a straight substitution: pull the company token out of the&lt;br&gt;
URL, drop it into the API pattern, get JSON. No key, no proxy, no browser.&lt;/p&gt;
&lt;h2&gt;
  
  
  Workday is the one that breaks the pattern
&lt;/h2&gt;

&lt;p&gt;Every other ATS needs one identifier. Workday needs three, and only one of them&lt;br&gt;
is guessable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://acme.wd5.myworkdayjobs.com/External_Career_Site
         ^^^^  ^^^                  ^^^^^^^^^^^^^^^^^^^
       tenant  shard                site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;tenant&lt;/strong&gt; is usually the company name, so you might guess it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shard&lt;/strong&gt; is &lt;code&gt;wd1&lt;/code&gt; through &lt;code&gt;wd12&lt;/code&gt; or higher, assigned when the account was provisioned. There is no pattern&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;site&lt;/strong&gt; is free text chosen by whoever set it up. &lt;code&gt;External_Career_Site&lt;/code&gt;, &lt;code&gt;NVIDIAExternalCareerSite&lt;/code&gt;, &lt;code&gt;Careers&lt;/code&gt; are all real&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why "just build the Workday URL from the company name" does not work.&lt;br&gt;
You need a real link from the company's careers site - any link, a single job&lt;br&gt;
posting is enough, because all three parts are in it.&lt;/p&gt;

&lt;p&gt;The endpoint itself is:&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;POST https://{tenant}.{shard}.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs
Content-Type: application/json

{"appliedFacets": {}, "limit": 20, "offset": 0, "searchText": ""}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three things that will waste your afternoon
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Workday pages at exactly 20.&lt;/strong&gt; Ask for &lt;code&gt;limit: 21&lt;/code&gt; and you get HTTP 200 with an&lt;br&gt;
empty list and &lt;code&gt;total&lt;/code&gt; still filled in. That looks identical to a company with no&lt;br&gt;
openings, so you conclude the board is empty and move on. Always page at 20.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workday's &lt;code&gt;postedOn&lt;/code&gt; is a display string, not a date.&lt;/strong&gt; It returns&lt;br&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; - and it returns&lt;br&gt;
them in whatever locale the endpoint feels like. Pin &lt;code&gt;Accept-Language: en-US&lt;/code&gt; on&lt;br&gt;
every request or your date parsing silently rots. Note also that &lt;code&gt;30+&lt;/code&gt; carries no&lt;br&gt;
date at all, so anything older than a month has no usable timestamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Greenhouse's jobs list has no departments.&lt;/strong&gt; &lt;code&gt;/jobs&lt;/code&gt; gives you titles and&lt;br&gt;
locations; departments live on a separate endpoint. If you need both you have to&lt;br&gt;
join them yourself, which is the single most common thing people miss.&lt;/p&gt;
&lt;h2&gt;
  
  
  Detecting it in code
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;PATTERNS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;greenhouse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?:job-)?boards\.greenhouse\.io/([\w-]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lever&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobs\.lever\.co/([\w-]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ashby&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobs\.ashbyhq\.com/([\w-]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apply\.workable\.com/([\w-]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recruitee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;([\w-]+)\.recruitee\.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smartrecruiters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;careers\.smartrecruiters\.com/([\w-]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;WORKDAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;([\w-]+)\.(wd\d+)\.myworkdayjobs\.com(?:/([^?#\s]*))?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;LOCALE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^[a-z]{2}(-[A-Za-z]{2,4})?$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WORKDAY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;segs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="c1"&gt;# The cxs API URL carries the site one segment after the tenant.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;segs&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="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;segs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cxs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;segs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;segs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="c1"&gt;# skip locale segments like en-US
&lt;/span&gt;                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;LOCALE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workday&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;site&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PATTERNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ats&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That locale loop matters more than it looks. Workday URLs shared from a&lt;br&gt;
non-English browser carry &lt;code&gt;/en-US/&lt;/code&gt; or &lt;code&gt;/fr-CA/&lt;/code&gt; before the site name, and if you&lt;br&gt;
take the first path segment blindly you get &lt;code&gt;site="en-US"&lt;/code&gt; and an endpoint that&lt;br&gt;
404s on every request. Walking past locale segments also means a deep link to a&lt;br&gt;
single posting works as input, which is usually the only link you can get someone&lt;br&gt;
to send you.&lt;/p&gt;

&lt;p&gt;Verified against these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite
  -&amp;gt; nvidia / wd5 / NVIDIAExternalCareerSite
acme.wd3.myworkdayjobs.com/fr-CA/External_Career_Site/job/Montreal/Dev_JR1
  -&amp;gt; acme / wd3 / External_Career_Site
salesforce.wd12.myworkdayjobs.com/wday/cxs/salesforce/External_Career_Site/jobs
  -&amp;gt; salesforce / wd12 / External_Career_Site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;Once you can identify the ATS from a URL, "monitor hiring at these 200 companies"&lt;br&gt;
stops being 200 scrapers and becomes one dispatcher plus seven thin clients. A&lt;br&gt;
company that is hiring is a company that is spending, which is why this data is&lt;br&gt;
worth having in the first place - headcount changes are one of the earliest&lt;br&gt;
public signals that a budget exists.&lt;/p&gt;

&lt;p&gt;I packaged each of these as a scraper on Apify, one per platform plus a combined&lt;br&gt;
one that auto-detects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Actor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All six, auto-detected&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/career-page-job-monitor" rel="noopener noreferrer"&gt;ATS Jobs &amp;amp; Hiring Signals&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/greenhouse-jobs-scraper" rel="noopener noreferrer"&gt;Greenhouse Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lever&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/lever-jobs-scraper" rel="noopener noreferrer"&gt;Lever Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ashby&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/ashby-jobs-scraper" rel="noopener noreferrer"&gt;Ashby Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workable&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/workable-jobs-scraper" rel="noopener noreferrer"&gt;Workable Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recruitee&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/recruitee-jobs-scraper" rel="noopener noreferrer"&gt;Recruitee Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SmartRecruiters&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/smartrecruiters-jobs-scraper" rel="noopener noreferrer"&gt;SmartRecruiters Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workday&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/workday-jobs-scraper" rel="noopener noreferrer"&gt;Workday Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The endpoints above are public either way - if you only need one board, the curl&lt;br&gt;
is genuinely all it takes.&lt;/p&gt;

&lt;p&gt;Earlier posts with the full field-by-field breakdown:&lt;br&gt;
&lt;a href="https://dev.to/udaninn/six-ats-platforms-publish-their-job-boards-as-open-json-here-are-the-endpoints-2d3k"&gt;the six open JSON boards&lt;/a&gt;&lt;br&gt;
and &lt;a href="https://dev.to/udaninn/workday-job-boards-have-a-json-api-too-its-just-better-hidden-23fl"&gt;Workday's hidden API&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>api</category>
      <category>python</category>
      <category>jobs</category>
    </item>
    <item>
      <title>Workday job boards have a JSON API too. It's just better hidden.</title>
      <dc:creator>UDANINN</dc:creator>
      <pubDate>Tue, 11 Aug 2026 13:21:35 +0000</pubDate>
      <link>https://dev.to/udaninn/workday-job-boards-have-a-json-api-too-its-just-better-hidden-23fl</link>
      <guid>https://dev.to/udaninn/workday-job-boards-have-a-json-api-too-its-just-better-hidden-23fl</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/udaninn/six-ats-platforms-publish-their-job-boards-as-open-json-here-are-the-endpoints-2d3k"&gt;the last post&lt;/a&gt;&lt;br&gt;
I listed six ATS platforms whose job boards are open JSON APIs — Greenhouse,&lt;br&gt;
Lever, Ashby, Workable, Recruitee, SmartRecruiters. The honest caveat at the&lt;br&gt;
bottom was that none of it covers Workday, which is where most of the Fortune&lt;br&gt;
500 actually lives.&lt;/p&gt;

&lt;p&gt;This post closes that gap. Workday careers sites are also a client-side app&lt;br&gt;
calling a JSON API, and you can call it directly — no key, no login, no&lt;br&gt;
headless browser. It is just harder to find, shaped differently, and mined&lt;br&gt;
with quieter traps than any of the six easy ones.&lt;/p&gt;

&lt;p&gt;Everything below was run live while writing this, in August 2026: NVIDIA&lt;br&gt;
(2,000 open roles), Salesforce (1,477), Adobe (800). As before, the counts are&lt;br&gt;
there to show the response shape, not as figures to check against later.&lt;/p&gt;
&lt;h2&gt;
  
  
  The endpoint
&lt;/h2&gt;

&lt;p&gt;Workday careers pages live at URLs like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://{tenant}.wd{N}.myworkdayjobs.com/{site}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind each one is a POST endpoint:&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;POST https://{tenant}.wd{N}.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs
Content-Type: application/json

{"appliedFacets": {}, "limit": 20, "offset": 0, "searchText": ""}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real examples, all verified today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nvidia.wd5.myworkdayjobs.com     /wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs   → 2,000 roles
salesforce.wd12.myworkdayjobs.com/wday/cxs/salesforce/External_Career_Site/jobs   → 1,477 roles
adobe.wd5.myworkdayjobs.com      /wday/cxs/adobe/external_experienced/jobs        →   800 roles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note it is a &lt;strong&gt;POST with a JSON body&lt;/strong&gt;, unlike every endpoint in the previous&lt;br&gt;
post. A GET returns nothing useful, which is one reason people conclude the&lt;br&gt;
API "doesn't exist" and reach for Selenium.&lt;/p&gt;

&lt;p&gt;Everything below is the method, so you can build this yourself. If you would&lt;br&gt;
rather not own the maintenance, I packaged it as a hosted scraper —&lt;br&gt;
&lt;a href="https://apify.com/practical_ophthalmologist_iuq/workday-jobs-scraper" rel="noopener noreferrer"&gt;Workday Jobs Scraper&lt;/a&gt;, callable from&lt;br&gt;
&lt;a href="https://apify.com/practical_ophthalmologist_iuq/workday-jobs-scraper/api/python" rel="noopener noreferrer"&gt;Python&lt;/a&gt; or &lt;a href="https://apify.com/practical_ophthalmologist_iuq/workday-jobs-scraper/api/javascript" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt;.&lt;br&gt;
Either way, the traps below are worth reading, because they are the same three&lt;br&gt;
that will bite your own implementation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Finding the three parts
&lt;/h2&gt;

&lt;p&gt;With Greenhouse you can guess the board token from the company domain.&lt;br&gt;
With Workday you cannot guess any of the three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{tenant}&lt;/code&gt; is usually the company name, but not reliably&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;wd{N}&lt;/code&gt; is a shard number — NVIDIA is on wd5, Salesforce on wd12 — and there
is no rule for which&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{site}&lt;/code&gt; is whatever the company named their career site:
&lt;code&gt;NVIDIAExternalCareerSite&lt;/code&gt;, &lt;code&gt;External_Career_Site&lt;/code&gt;, &lt;code&gt;external_experienced&lt;/code&gt;.
Three companies, three naming conventions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The good news: &lt;strong&gt;every Workday job link contains all three.&lt;/strong&gt; Take any posting&lt;br&gt;
URL a company shares —&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite/job/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;— and you can read tenant (&lt;code&gt;nvidia&lt;/code&gt;), shard (&lt;code&gt;wd5&lt;/code&gt;) and site&lt;br&gt;
(&lt;code&gt;NVIDIAExternalCareerSite&lt;/code&gt;) straight out of it, dropping the locale segment&lt;br&gt;
(&lt;code&gt;en-US&lt;/code&gt;) if present. So the practical flow is: given a company, find one job&lt;br&gt;
link (their careers page, a LinkedIn posting, a Google result), decompose it&lt;br&gt;
once, and you have the API forever.&lt;/p&gt;
&lt;h2&gt;
  
  
  Trap 1: &lt;code&gt;limit&lt;/code&gt; above 20 returns zero rows, not an error
&lt;/h2&gt;

&lt;p&gt;This one cost me the most time, and I have not seen it written down anywhere.&lt;br&gt;
The endpoint pages at 20 records. If you ask for more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;jobs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;jobs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;jobs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No error, no warning, HTTP 200, &lt;code&gt;total&lt;/code&gt; still says 1,477 — and an empty&lt;br&gt;
&lt;code&gt;jobPostings&lt;/code&gt; array. If your code asks for 100 per page (a perfectly normal&lt;br&gt;
thing to do, since SmartRecruiters takes 100), every Workday company comes&lt;br&gt;
back as "not hiring". Silent wrong answers again: the response looks exactly&lt;br&gt;
like a company with no openings.&lt;/p&gt;

&lt;p&gt;Page with &lt;code&gt;limit: 20&lt;/code&gt; and step &lt;code&gt;offset&lt;/code&gt; by 20 until you reach &lt;code&gt;total&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;workday_jobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shard&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.myworkdayjobs.com/wday/cxs/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;appliedFacets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;offset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;searchText&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&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="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobPostings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trap 2: &lt;code&gt;postedOn&lt;/code&gt; is not a date — it is a sentence, in your language
&lt;/h2&gt;

&lt;p&gt;The list response carries five fields per job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;externalPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;locationsText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;postedOn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;bulletFields&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;postedOn&lt;/code&gt; looks like a date field. It is a &lt;strong&gt;localized display string&lt;/strong&gt;. The&lt;br&gt;
same Salesforce posting, same endpoint, different &lt;code&gt;Accept-Language&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Accept-Language: en-US  →  "postedOn": "Posted Today",   "locationsText": "2 Locations"
Accept-Language: ko     →  "postedOn": "오늘 공고",        "locationsText": "2개 근무지"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I only caught this because my browser is set to Korean. If you parse&lt;br&gt;
&lt;code&gt;postedOn&lt;/code&gt; as a date, your code works on your machine and breaks for anyone&lt;br&gt;
whose locale differs — or breaks next week when "Posted Today" becomes&lt;br&gt;
"Posted 30+ Days Ago", which is the &lt;em&gt;only&lt;/em&gt; granularity you get past a month.&lt;/p&gt;

&lt;p&gt;Two consequences: always send an explicit &lt;code&gt;Accept-Language&lt;/code&gt;, and if you need a&lt;br&gt;
real date, get it from the detail endpoint below (&lt;code&gt;startDate&lt;/code&gt; is an actual&lt;br&gt;
&lt;code&gt;YYYY-MM-DD&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;
  
  
  The detail endpoint
&lt;/h2&gt;

&lt;p&gt;Each list item has an &lt;code&gt;externalPath&lt;/code&gt;. Append it to the site base with an&lt;br&gt;
&lt;code&gt;Accept: application/json&lt;/code&gt; header:&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://{tenant}.wd{N}.myworkdayjobs.com/wday/cxs/{tenant}/{site}{externalPath}
Accept: application/json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns the full record: &lt;code&gt;jobDescription&lt;/code&gt; (HTML), &lt;code&gt;timeType&lt;/code&gt;&lt;br&gt;
("Full time"), &lt;code&gt;startDate&lt;/code&gt; (a real date at last), &lt;code&gt;jobReqId&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;additionalLocations&lt;/code&gt;. Without the &lt;code&gt;Accept&lt;/code&gt; header you get the HTML shell of&lt;br&gt;
the careers page instead — the same URL serves both.&lt;/p&gt;

&lt;p&gt;One request per job, so for a 2,000-role tenant that is 2,000 extra requests.&lt;br&gt;
Fetch details only for the postings you actually care about, after filtering&lt;br&gt;
the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: no CORS — this is server-side only
&lt;/h2&gt;

&lt;p&gt;The six platforms in the previous post mostly tolerate browser calls; Workable&lt;br&gt;
even sends explicit CORS headers. Workday does not. A cross-origin &lt;code&gt;fetch&lt;/code&gt;&lt;br&gt;
from a browser dies before you see a status code. Call it from a server, a&lt;br&gt;
script, a notebook — anything but someone else's web page. (Same-origin works,&lt;br&gt;
which is how the careers page itself uses it.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;This is the API behind the public careers page, read-only and unauthenticated
— but unlike Greenhouse's boards API it is &lt;strong&gt;not documented as a public
contract&lt;/strong&gt;. Workday can reshape it without notice. Build accordingly:
tolerate missing fields, alert on shape changes.&lt;/li&gt;
&lt;li&gt;The list response is thin — five fields. Department, salary and employment
type all live behind the per-job detail request.&lt;/li&gt;
&lt;li&gt;Some tenants run heavily customized sites; the three above are typical, not
universal.&lt;/li&gt;
&lt;li&gt;Page politely. It is one POST per 20 jobs; there is no reason to hammer it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Update: it is built now
&lt;/h2&gt;

&lt;p&gt;I went ahead and packaged this as a ready-to-run scraper:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/workday-jobs-scraper" rel="noopener noreferrer"&gt;Workday Jobs Scraper &amp;amp; API on Apify&lt;/a&gt;&lt;/strong&gt; —&lt;br&gt;
paste any careers or job-posting URL and it decomposes tenant, shard and&lt;br&gt;
site itself, pages at the mandatory 20, pins &lt;code&gt;Accept-Language: en-US&lt;/code&gt;, and&lt;br&gt;
returns the same normalised schema as &lt;a href="https://apify.com/practical_ophthalmologist_iuq/career-page-job-monitor" rel="noopener noreferrer"&gt;the six easy platforms&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For one company you should still just use the code above - it is a single&lt;br&gt;
loop. The Actor earns its keep at the point where that stops being true:&lt;br&gt;
many tenants, change detection, schedules.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>webscraping</category>
      <category>hiring</category>
    </item>
    <item>
      <title>Six ATS platforms publish their job boards as open JSON. Here are the endpoints.</title>
      <dc:creator>UDANINN</dc:creator>
      <pubDate>Mon, 10 Aug 2026 12:12:40 +0000</pubDate>
      <link>https://dev.to/udaninn/six-ats-platforms-publish-their-job-boards-as-open-json-here-are-the-endpoints-2d3k</link>
      <guid>https://dev.to/udaninn/six-ats-platforms-publish-their-job-boards-as-open-json-here-are-the-endpoints-2d3k</guid>
      <description>&lt;p&gt;Most "job scraping" tutorials teach you to render a careers page in a headless&lt;br&gt;
browser and pick apart the HTML. For the majority of company career pages, that&lt;br&gt;
is unnecessary. The page you are trying to scrape is itself a client-side app&lt;br&gt;
calling a public JSON API — and you can call that API directly.&lt;/p&gt;

&lt;p&gt;No key. No proxy. No anti-bot. Just JSON.&lt;/p&gt;

&lt;p&gt;Below are the six applicant tracking systems that cover most of the startup and&lt;br&gt;
mid-market hiring world, the exact endpoint each one exposes, and the traps I&lt;br&gt;
hit building against all six.&lt;/p&gt;

&lt;p&gt;Every endpoint here was run live while writing this, in August 2026. The job&lt;br&gt;
counts are from that run and will have drifted by the time you read it — they&lt;br&gt;
are there to show the shape of the response, not as figures to check against.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the APIs are open in the first place
&lt;/h2&gt;

&lt;p&gt;When a company buys Greenhouse or Lever, they get a hosted job board, and they&lt;br&gt;
usually want the openings to appear on their own marketing site under their own&lt;br&gt;
design. So the ATS ships a public read API for exactly that purpose.&lt;/p&gt;

&lt;p&gt;That is the important bit: this is not an oversight to be exploited. The&lt;br&gt;
endpoint exists to be read by third parties. It is documented, versioned, and&lt;br&gt;
stable — the same properties that make it pleasant to build on.&lt;/p&gt;

&lt;p&gt;The practical consequence is that reading job data is one of the few remaining&lt;br&gt;
corners of the web where you do not need a proxy budget.&lt;/p&gt;
&lt;h2&gt;
  
  
  The six endpoints
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Greenhouse
&lt;/h3&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/{token}/jobs
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The token is the slug in &lt;code&gt;job-boards.greenhouse.io/{token}&lt;/code&gt;. &lt;code&gt;stripe&lt;/code&gt; returns&lt;br&gt;
around 550 openings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trap:&lt;/strong&gt; the &lt;code&gt;/jobs&lt;/code&gt; response has no department on it at all — the keys are&lt;br&gt;
&lt;code&gt;absolute_url, data_compliance, internal_job_id, location, metadata, id,&lt;br&gt;
updated_at, requisition_id, title, company_name, first_published, language,&lt;br&gt;
application_deadline&lt;/code&gt;, and that is the whole list. If you want departments you&lt;br&gt;
have to call a second endpoint and join:&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/{token}/departments
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each department carries its own &lt;code&gt;jobs&lt;/code&gt; array, so you build an id → department&lt;br&gt;
map and merge. Miss this and every record silently comes back with an empty&lt;br&gt;
department, which looks like the companies just didn't fill it in.&lt;/p&gt;
&lt;h3&gt;
  
  
  Lever
&lt;/h3&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/{token}?mode=json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;leverdemo&lt;/code&gt; is Lever's own sandbox board and returns 388 postings, which makes&lt;br&gt;
it a good fixture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The trap:&lt;/strong&gt; Lever returns &lt;code&gt;createdAt&lt;/code&gt; as &lt;strong&gt;epoch milliseconds&lt;/strong&gt;, while&lt;br&gt;
Greenhouse and Ashby return ISO 8601 strings. If you normalise the others and&lt;br&gt;
forget this one, a "posted in the last 7 days" filter doesn't error — it just&lt;br&gt;
quietly returns nothing for every Lever board, because an epoch-millisecond&lt;br&gt;
integer parsed as a year is not a date anywhere near today. Silent wrong answers&lt;br&gt;
are worse than crashes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ashby
&lt;/h3&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/{token}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Add &lt;code&gt;?includeCompensation=true&lt;/code&gt; and you get published salary ranges, which&lt;br&gt;
Ashby exposes far more consistently than the others — on Ramp's board, 115 of&lt;br&gt;
121 postings came back with a real figure, formatted like&lt;br&gt;
&lt;code&gt;$211.4K – $290.6K • Offers Equity&lt;/code&gt;. On the other five platforms salary is the&lt;br&gt;
exception rather than the rule, so if compensation data is what you are after,&lt;br&gt;
Ashby boards are where it actually lives.&lt;/p&gt;
&lt;h3&gt;
  
  
  Workable
&lt;/h3&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://apply.workable.com/api/v1/widget/accounts/{token}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the endpoint powering Workable's embeddable widget. It gives you&lt;br&gt;
department, employment type and a remote flag in one call.&lt;/p&gt;
&lt;h3&gt;
  
  
  Recruitee
&lt;/h3&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://{token}.recruitee.com/api/offers/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;The trap:&lt;/strong&gt; the response contains &lt;code&gt;options_cover_letter&lt;/code&gt;. It is a boolean —&lt;br&gt;
whether the application form asks for a cover letter. I mapped it to&lt;br&gt;
&lt;code&gt;employmentType&lt;/code&gt; at one point on nothing but name-shaped optimism, and produced&lt;br&gt;
a column full of &lt;code&gt;true&lt;/code&gt;/&lt;code&gt;false&lt;/code&gt; where job types should have been. Read the&lt;br&gt;
values, not the field names.&lt;/p&gt;
&lt;h3&gt;
  
  
  SmartRecruiters
&lt;/h3&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.smartrecruiters.com/v1/companies/{token}/postings?limit=100&amp;amp;offset=0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two traps here, and they cost me the most time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pagination.&lt;/strong&gt; The response includes &lt;code&gt;totalFound&lt;/code&gt;, and it will happily tell you&lt;br&gt;
&lt;code&gt;4753&lt;/code&gt; while handing you 100 records. If you don't page on &lt;code&gt;offset&lt;/code&gt;, you silently&lt;br&gt;
truncate every large employer to their first 100 openings. No error, no warning.&lt;br&gt;
This is the kind of bug that survives a full test suite, because 100 records&lt;br&gt;
looks like success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case sensitivity, and identifiers that are not the company name.&lt;/strong&gt; The&lt;br&gt;
identifier is case sensitive, and it is frequently not what you would guess:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You would guess&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Live identifier&lt;/th&gt;
&lt;th&gt;Openings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Bosch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;empty&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BoschGroup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4,753&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Ubisoft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;empty&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ubisoft2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;282&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Visa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;works&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Visa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the failure mode: a wrong identifier returns &lt;strong&gt;HTTP 200 with an empty&lt;br&gt;
list&lt;/strong&gt;, not a 404. So "this company isn't on SmartRecruiters" and "you spelled&lt;br&gt;
it wrong" are indistinguishable from the response alone. I removed &lt;code&gt;Bosch&lt;/code&gt; from&lt;br&gt;
a set of examples once, having concluded it wasn't available, when the real&lt;br&gt;
answer was that I had the name wrong.&lt;/p&gt;

&lt;p&gt;That is all six endpoints. The rest of this post is the awkward part — turning&lt;br&gt;
company names into board slugs, surviving one malformed record, and polling on a&lt;br&gt;
schedule. If you would rather not maintain that, it is packaged as a hosted&lt;br&gt;
scraper that auto-detects the platform:&lt;br&gt;
&lt;a href="https://apify.com/practical_ophthalmologist_iuq/career-page-job-monitor" rel="noopener noreferrer"&gt;ATS Jobs &amp;amp; Hiring Signals&lt;/a&gt;, callable from&lt;br&gt;
&lt;a href="https://apify.com/practical_ophthalmologist_iuq/career-page-job-monitor/api/python" rel="noopener noreferrer"&gt;Python&lt;/a&gt; or &lt;a href="https://apify.com/practical_ophthalmologist_iuq/career-page-job-monitor/api/javascript" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The thing nobody tells you: people don't have board tokens
&lt;/h2&gt;

&lt;p&gt;Every one of these endpoints wants an ATS board slug. Nobody has a list of ATS&lt;br&gt;
board slugs. What people actually have — sales teams, recruiters, job hunters —&lt;br&gt;
is a list of &lt;strong&gt;company domains&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So the useful move is to accept whatever someone pastes and work it out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;stripe.com, www.stripe.com and https://stripe.com/careers
    all resolve to `stripe`.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^\w+://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^www\.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&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="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, because dropping the TLD is a guess and not a rule, keep a fallback:&lt;br&gt;
&lt;code&gt;shield.ai&lt;/code&gt; becomes &lt;code&gt;shield&lt;/code&gt;, but the actual Lever board is &lt;code&gt;shieldai&lt;/code&gt; (433&lt;br&gt;
openings). So generate both spellings and try the second only after the first&lt;br&gt;
has missed everywhere.&lt;/p&gt;

&lt;p&gt;The cost of the fallback is one extra request on a miss. The benefit is that&lt;br&gt;
users stop having to look anything up. That trade is worth it every time.&lt;/p&gt;
&lt;h2&gt;
  
  
  One malformed record should not cost you the other 499
&lt;/h2&gt;

&lt;p&gt;Every one of these APIs will eventually hand you something you didn't expect:&lt;br&gt;
&lt;code&gt;jobs: null&lt;/code&gt; instead of an empty list, a string where a dict belongs, a null&lt;br&gt;
inside an otherwise fine array. If you build each record inside a loop with no&lt;br&gt;
isolation, one bad posting takes down the whole board.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;as_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;          &lt;span class="c1"&gt;# one bad posting, not a dead run
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The title/URL check matters as much as the try block. A record with no title and&lt;br&gt;
no link is not a job anyone can act on, and passing it downstream just moves the&lt;br&gt;
problem somewhere it is harder to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detection across all six
&lt;/h2&gt;

&lt;p&gt;Given a bare company name, you can find the board by trying platforms in&lt;br&gt;
sequence and stopping at the first hit. Order matters for speed — Greenhouse and&lt;br&gt;
Lever cover the most companies, so put them first.&lt;/p&gt;

&lt;p&gt;One subtlety worth getting right: &lt;strong&gt;a valid board with zero openings is a real&lt;br&gt;
answer, not a miss.&lt;/strong&gt; A company that genuinely isn't hiring should return an&lt;br&gt;
empty list, not fall through to the next platform and eventually report "not&lt;br&gt;
found". Those are different facts and users need to tell them apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Only these six. Companies on Taleo, iCIMS, SuccessFactors or a hand-rolled
careers page are not covered by any of this.&lt;/li&gt;
&lt;li&gt;Private or password-protected boards are not accessible, and shouldn't be.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;department&lt;/code&gt; and &lt;code&gt;team&lt;/code&gt; are only as good as what the company filled in.&lt;/li&gt;
&lt;li&gt;Salary shows up only where the company published it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bit I'd add if you are polling this on a schedule
&lt;/h2&gt;

&lt;p&gt;If you run any of this daily, diff against the previous run rather than&lt;br&gt;
re-reading the whole board. Keep a snapshot of the ids you saw, and emit only&lt;br&gt;
the ones that appeared and the ones that vanished. Two details that turned out&lt;br&gt;
to matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key the snapshot to the exact query.&lt;/strong&gt; Change your company list or a filter
and you should start a fresh baseline. Otherwise every job you simply stopped
asking about gets reported as newly closed, which is a wrong answer rather
than a noisy one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A job that disappeared cannot be re-fetched.&lt;/strong&gt; Report it from the snapshot —
company, title, link — and leave the rest of the fields out instead of showing
a stale copy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you'd rather not build it
&lt;/h2&gt;

&lt;p&gt;I packaged all six adapters — normalisation, the timestamp fix, the pagination,&lt;br&gt;
the domain-to-token fallback, the change detection above — as Apify Actors. You&lt;br&gt;
paste company domains and get one flat schema back, exportable as&lt;br&gt;
CSV/Excel/JSON or callable as an API.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Actor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All six, auto-detected&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/career-page-job-monitor" rel="noopener noreferrer"&gt;ATS Jobs &amp;amp; Hiring Signals&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/greenhouse-jobs-scraper" rel="noopener noreferrer"&gt;Greenhouse Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lever&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/lever-jobs-scraper" rel="noopener noreferrer"&gt;Lever Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ashby&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/ashby-jobs-scraper" rel="noopener noreferrer"&gt;Ashby Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workable&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/workable-jobs-scraper" rel="noopener noreferrer"&gt;Workable Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recruitee&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/recruitee-jobs-scraper" rel="noopener noreferrer"&gt;Recruitee Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SmartRecruiters&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/smartrecruiters-jobs-scraper" rel="noopener noreferrer"&gt;SmartRecruiters Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workday (&lt;a href="https://dev.to/udaninn/workday-job-boards-have-a-json-api-too-its-just-better-hidden-23fl"&gt;part 2&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/practical_ophthalmologist_iuq/workday-jobs-scraper" rel="noopener noreferrer"&gt;Workday Jobs Scraper&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Source for the adapters is on&lt;br&gt;
&lt;a href="https://github.com/udaninn/career-page-job-monitor" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if you just&lt;br&gt;
want to read how a particular platform was handled.&lt;/p&gt;

&lt;p&gt;But the endpoints above are the whole trick. If you only need one platform, a&lt;br&gt;
40-line script will do it, and now you know which four bugs to write tests for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 2 is up: &lt;a href="https://dev.to/udaninn/workday-job-boards-have-a-json-api-too-its-just-better-hidden-23fl"&gt;Workday job boards have a JSON API too. It's just better hidden.&lt;/a&gt; — the enterprise ATS this post deliberately left out, with three traps that are documented nowhere else.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>webscraping</category>
      <category>hiring</category>
    </item>
  </channel>
</rss>
