<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Open Job Data]]></title><description><![CDATA[Open Job Data]]></description><link>https://jobdata.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 28 Oct 2026 12:06:39 GMT</lastBuildDate><atom:link href="https://jobdata.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Lever Postings API: pull every open job from a jobs.lever.co company with curl]]></title><description><![CDATA[Finding the site name, the US and EU hosts, pagination, filters, salary ranges and the fields that trip people up]]></description><link>https://jobdata.hashnode.dev/lever-postings-api-pull-every-open-job-from-a-jobs-lever-co-company-with-curl</link><guid isPermaLink="true">https://jobdata.hashnode.dev/lever-postings-api-pull-every-open-job-from-a-jobs-lever-co-company-with-curl</guid><category><![CDATA[api]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Jordan Belfort]]></dc:creator><pubDate>Thu, 22 Oct 2026 15:14:05 GMT</pubDate><content:encoded><![CDATA[<p>Lever is the applicant tracking system behind the <code>jobs.lever.co</code> careers pages, and it publishes every open job through a public, read-only endpoint: the Postings API. No key, no sign-up. This is a walkthrough of it with the calls I ran on 22 October 2026 against Zoox, Spotify and Palantir, and what came back.</p>
<p>It's the feed Lever runs so companies can show their jobs on their own sites, so it's meant to be read. Cache what you fetch and don't poll it in a tight loop.</p>
<h2>1. Find the site name, and the region</h2>
<p>Every call starts with the company's <strong>site name</strong>: the first path segment of its Lever board. <code>jobs.lever.co/zoox</code> means the site is <code>zoox</code>. Job links look like <code>jobs.lever.co/zoox/&lt;uuid&gt;</code>, so any "Apply" link gives it away.</p>
<p>Lever has two regions. A board at <code>jobs.eu.lever.co/&lt;site&gt;</code> is served from <code>api.eu.lever.co</code>, not <code>api.lever.co</code>. The trap is that the wrong host doesn't always say so. Lever's own board lives on the EU host:</p>
<pre><code>$ curl -s -w "\n%{http_code}\n" "https://api.lever.co/v0/postings/lever?mode=json"
[]
200

$ curl -s "https://api.eu.lever.co/v0/postings/lever?mode=json"   # 6 postings
</code></pre>
<p>An empty array with a 200 looks like "no open jobs". Before you conclude that, try the other host.</p>
<h2>2. List the jobs: GET /v0/postings/{site}?mode=json</h2>
<pre><code class="language-bash">curl "https://api.lever.co/v0/postings/zoox?mode=json"
</code></pre>
<p>The response is a bare JSON array, no wrapper and no total. (Today it answered JSON even without <code>mode=json</code>; I keep the parameter in anyway.) Zoox returned 236 postings, Spotify 81, Palantir 304 (a 5.9 MB response, because every posting carries its full description). One Zoox posting, trimmed:</p>
<pre><code class="language-json">{
  "id": "e8a190f0-7a89-4c11-a6a0-452ff6234749",
  "text": "Administrative Business Partner, Floater",
  "createdAt": 1791307664932,
  "categories": {
    "commitment": "Full-time",
    "department": "Executive Office",
    "location": "Foster City, CA",
    "team": "Executive Office",
    "allLocations": ["Foster City, CA"]
  },
  "country": "US",
  "workplaceType": "hybrid",
  "salaryRange": {
    "min": 123000,
    "max": 168000,
    "currency": "USD",
    "interval": "per-year-salary"
  },
  "hostedUrl": "https://jobs.lever.co/zoox/e8a190f0-7a89-4c11-a6a0-452ff6234749",
  "applyUrl": "https://jobs.lever.co/zoox/e8a190f0-7a89-4c11-a6a0-452ff6234749/apply"
}
</code></pre>
<p>The title is <code>text</code>, not <code>title</code>. <code>hostedUrl</code> is the page a candidate should see.</p>
<h3>createdAt is milliseconds</h3>
<p><code>createdAt</code> is a Unix timestamp in <strong>milliseconds</strong>. <code>1791307664932</code> is 2026-10-06. Feed it to a function expecting seconds and you land tens of thousands of years in the future. Divide by 1000 first:</p>
<pre><code class="language-python">from datetime import datetime, timezone
datetime.fromtimestamp(1791307664932 / 1000, tz=timezone.utc)
# 2026-10-06 17:27:44.932000+00:00
</code></pre>
<p>It's the only date in the response. There's no "updated" field. And it's not always recent: Palantir's oldest open posting has a <code>createdAt</code> in December 2009, while 5 of its 304 were created in the past week.</p>
<h3>Pagination: skip and limit</h3>
<p>The plain call returns everything. To page, add <code>skip</code> and <code>limit</code>:</p>
<pre><code class="language-bash">curl "https://api.lever.co/v0/postings/zoox?mode=json&amp;skip=0&amp;limit=3"
curl "https://api.lever.co/v0/postings/zoox?mode=json&amp;skip=3&amp;limit=3"
</code></pre>
<p>The two pages returned postings 1-3 and 4-6 of the full list, in the same order. There's no total or "next" marker: keep going until a page comes back shorter than <code>limit</code>.</p>
<h2>3. Filters</h2>
<p>The endpoint filters server-side on the <code>categories</code> values. All of these ran against Spotify's 81 postings:</p>
<table>
<thead>
<tr>
<th>Query</th>
<th>Postings</th>
</tr>
</thead>
<tbody><tr>
<td><code>team=Platform</code></td>
<td>16</td>
</tr>
<tr>
<td><code>department=Engineering</code></td>
<td>32</td>
</tr>
<tr>
<td><code>commitment=Permanent</code></td>
<td>75</td>
</tr>
<tr>
<td><code>location=London</code></td>
<td>37</td>
</tr>
<tr>
<td><code>location=London&amp;location=Stockholm</code></td>
<td>41</td>
</tr>
<tr>
<td><code>location=london</code></td>
<td>0</td>
</tr>
</tbody></table>
<p>Three things fall out of that table:</p>
<ul>
<li><strong>Matching is exact and case-sensitive.</strong> <code>london</code> matches nothing. Take values from the data, not from a guess.</li>
<li><strong>Repeating a parameter ORs the values.</strong></li>
<li><strong><code>location</code> matches <code>allLocations</code>, not just the primary location.</strong> Only 31 Spotify postings have London as <code>categories.location</code>, but 37 come back, because multi-location jobs list it in <code>allLocations</code>.</li>
</ul>
<p><code>level</code> works too: Zoox has 20 postings with level "All Levels", and <code>level=All%20Levels</code> returned those 20. Most companies don't fill it in. Palantir and Spotify have no <code>level</code> on any posting, and Palantir has no <code>department</code> either, only <code>team</code>.</p>
<p><code>group=team</code> changes the shape of the response. Instead of a flat array you get one object per team, each with <code>title</code> and <code>postings</code>:</p>
<pre><code class="language-bash">curl "https://api.lever.co/v0/postings/spotify?mode=json&amp;group=team"
</code></pre>
<p>Spotify came back as 23 groups ("Accounting", "Experience", ...) holding the same 81 postings.</p>
<h2>4. One posting: GET /v0/postings/{site}/{id}</h2>
<pre><code class="language-bash">curl "https://api.lever.co/v0/postings/zoox/e8a190f0-7a89-4c11-a6a0-452ff6234749"
</code></pre>
<p>This returns a single object with the same fields as in the list. No <code>mode=json</code> is needed here.</p>
<h2>5. The fields worth knowing</h2>
<ul>
<li><strong><code>workplaceType</code></strong>: <code>onsite</code>, <code>hybrid</code> or <code>remote</code>. Spotify had 40 hybrid, 28 remote and 13 onsite. Unlike Greenhouse, Lever gives you a real remote flag.</li>
<li><strong><code>salaryRange</code></strong>: present only when the company publishes it. 222 of Zoox's 236 postings had one (mostly USD per year, 8 <code>per-hour-wage</code>, 1 in GBP); Spotify and Palantir had none. Check <code>interval</code> before you compare numbers. Postings with a range usually also carry <code>salaryDescription</code> / <code>salaryDescriptionPlain</code>, the company's free-text pay note.</li>
<li><strong><code>description</code> vs <code>descriptionPlain</code></strong>: the intro as HTML and as plain text. Unlike Greenhouse, the HTML isn't double-escaped, so it renders as is.</li>
<li><strong><code>lists</code></strong>: the bulleted sections, each with a <code>text</code> heading and HTML <code>content</code> of <code>&lt;li&gt;</code> items. The Zoox posting has "Responsibilities", "Qualifications" and "Bonus Qualifications". To rebuild the full job description, concatenate <code>description</code>, each list, and <code>additional</code>.</li>
<li><strong><code>additional</code> / <code>additionalPlain</code></strong>: the closing text, often the equal-opportunity statement or office rules ("this role does require an onsite presence in our Foster City, CA office between 3-5 days per week").</li>
<li><strong><code>opening</code> and <code>descriptionBody</code></strong>: newer fields that split the intro, also with <code>Plain</code> versions. Usage varies: <code>opening</code> was filled on 300 of Palantir's 304 postings, 4 of Spotify's 81 and none of Zoox's.</li>
</ul>
<h2>6. Wrong site, wrong id</h2>
<pre><code>$ curl -s -w "\n%{http_code}\n" "https://api.lever.co/v0/postings/notarealcompany12345?mode=json"
{"ok":false,"error":"Document not found"}
404
</code></pre>
<p>A wrong posting id on a real site gives the same 404 body. But as section 1 showed, a site that exists only on the other region can return <code>[]</code> with a 200. Mistral returned <code>[]</code> on the US host and 404 on the EU host. So: 404 means wrong name; <code>[]</code> means no jobs on this host, so check the other one.</p>
<h2>7. The apply endpoint: leave it alone</h2>
<p>The posting URL also accepts a POST, which is how a custom careers page submits an application:</p>
<pre><code>$ curl -s -X POST -w "\n%{http_code}\n" https://api.lever.co/v0/postings/zoox/e8a190f0-7a89-4c11-a6a0-452ff6234749
{"ok":false,"error":"You need an API key. Please contact support@lever.co for a key"}
403
</code></pre>
<p>That key belongs to the company. Stick to the read side.</p>
<h2>Summary</h2>
<table>
<thead>
<tr>
<th>Call</th>
<th>Returns</th>
</tr>
</thead>
<tbody><tr>
<td><code>GET /v0/postings/{site}?mode=json</code></td>
<td>every open posting, bare array</td>
</tr>
<tr>
<td><code>...&amp;skip=N&amp;limit=M</code></td>
<td>one page</td>
</tr>
<tr>
<td><code>...&amp;team=</code> <code>department=</code> <code>location=</code> <code>commitment=</code> <code>level=</code></td>
<td>exact-match filters, repeatable</td>
</tr>
<tr>
<td><code>...&amp;group=team</code></td>
<td>postings grouped by a category</td>
</tr>
<tr>
<td><code>GET /v0/postings/{site}/{id}</code></td>
<td>one posting</td>
</tr>
<tr>
<td><code>api.eu.lever.co/...</code></td>
<td>the same, for <code>jobs.eu.lever.co</code> boards</td>
</tr>
</tbody></table>
<h2>If you need more than Lever</h2>
<p>One Lever client is an afternoon's work. If you're watching companies across several ATSs, I maintain an Apify Actor, <a href="https://apify.com/jordan-belfort/ats-jobs-api">Company Jobs API</a>, that reads Lever (US and EU) plus Greenhouse, Workday, Ashby, Workable, SmartRecruiters, Recruitee and Personio and returns all of them in one schema. You can pass a careers URL like <code>https://jobs.lever.co/zoox</code> or just a company name or domain, and it finds the board. It costs $0.001 per job returned; jobs your filters drop and companies it can't find are free. Turn on <em>Only new jobs</em>, schedule it daily, and each run returns only postings that appeared since the last one.</p>
]]></content:encoded></item><item><title><![CDATA[Greenhouse Job Board API: pull every open job from a boards.greenhouse.io company with curl]]></title><description><![CDATA[Finding the board token, the jobs endpoint, descriptions, salary ranges, application questions, departments and offices]]></description><link>https://jobdata.hashnode.dev/greenhouse-job-board-api-pull-every-open-job-from-a-boards-greenhouse-io-company-with-curl</link><guid isPermaLink="true">https://jobdata.hashnode.dev/greenhouse-job-board-api-pull-every-open-job-from-a-boards-greenhouse-io-company-with-curl</guid><category><![CDATA[api]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Jordan Belfort]]></dc:creator><pubDate>Thu, 22 Oct 2026 15:07:06 GMT</pubDate><content:encoded><![CDATA[<p>Greenhouse is the applicant tracking system behind a large share of tech companies' careers pages, and it publishes every open job through a public, read-only API: the Job Board API at <code>boards-api.greenhouse.io</code>. No key, no sign-up. This is a walkthrough of it with the calls I ran on 22 October 2026 against Discord, Stripe and Airbnb, and what came back.</p>
<p>It's the feed Greenhouse runs so careers pages and job boards can show a company's jobs, so it's meant to be read. Still: cache what you fetch and don't poll it in a tight loop.</p>
<h2>1. Find the board token</h2>
<p>Every Greenhouse call starts with the company's <strong>board token</strong>, a short slug like <code>discord</code>. There are three places to find it.</p>
<p><strong>The board URL.</strong> Older links look like <code>boards.greenhouse.io/&lt;token&gt;</code>, newer ones <code>job-boards.greenhouse.io/&lt;token&gt;</code>. The old host now redirects to the new one:</p>
<pre><code>$ curl -s -o /dev/null -w "%{http_code} -&gt; %{redirect_url}\n" https://boards.greenhouse.io/discord
301 -&gt; https://job-boards.greenhouse.io/discord
</code></pre>
<p>Either way, the token is the first path segment.</p>
<p><strong>A <code>gh_jid</code> in the company's own careers URL.</strong> Many companies keep the jobs on their own domain and embed Greenhouse. The giveaway is a <code>gh_jid</code> query parameter: that's the Greenhouse job id. Stripe's jobs link to <code>https://stripe.com/jobs/search?gh_jid=8172510</code> and Airbnb's to <code>https://careers.airbnb.com/positions/7789554?gh_jid=7789554</code>. The id tells you it's Greenhouse; it doesn't tell you the token, so try the company name (<code>stripe</code>, <code>airbnb</code>) against the API in step 2. A 200 means you guessed right.</p>
<p><strong>A <code>grnh.se</code> short link.</strong> That domain is Greenhouse's (the bare <code>https://grnh.se/</code> redirects to <code>www.greenhouse.io</code>). If a job link uses it, follow the redirect and read the token or <code>gh_jid</code> from where it lands.</p>
<p>If the page embeds the board with an iframe, the embed URL carries the token as <code>for=</code>: <code>boards.greenhouse.io/embed/job_board?for=discord</code>.</p>
<h2>2. List the jobs: GET /v1/boards/{token}/jobs</h2>
<pre><code class="language-bash">curl "https://boards-api.greenhouse.io/v1/boards/discord/jobs"
</code></pre>
<p>The response has two keys, <code>jobs</code> and <code>meta</code>. Discord returned 44 jobs and <code>"meta": {"total": 44}</code>. One of them, trimmed:</p>
<pre><code class="language-json">{
  "absolute_url": "https://job-boards.greenhouse.io/discord/jobs/8806482002",
  "internal_job_id": 6524134002,
  "location": { "name": "San Francisco Bay Area" },
  "id": 8806482002,
  "updated_at": "2026-10-20T20:27:20-04:00",
  "requisition_id": "R-107406",
  "title": "Commercial Policy Lead",
  "company_name": "Discord",
  "first_published": "2026-09-15T12:52:11-04:00",
  "language": "en",
  "application_deadline": null
}
</code></pre>
<p>Every job also carries <code>metadata</code> (the company's custom fields, here "Featured on Website" and "Careers Site Department"), <code>data_compliance</code> (GDPR consent flags) and, for most, <code>education</code>. <code>absolute_url</code> is the link a candidate should follow, and it points wherever the company hosts its board: for Stripe it's the <code>stripe.com/...?gh_jid=</code> URL, not a greenhouse.io one.</p>
<p>Three things to know:</p>
<ul>
<li><strong>There is no pagination.</strong> One request returns the whole board. Stripe returned 725 jobs in one 452 KB response, with <code>meta.total</code> 725.</li>
<li><strong>There is no auth</strong> on any of the read endpoints in this article.</li>
<li><strong>The plain list has no departments or offices.</strong> Those fields only appear with <code>?content=true</code> or on the single-job endpoint (sections 3 and 4).</li>
</ul>
<h3>Which date to use</h3>
<p>There are two timestamps, and they mean different things:</p>
<ul>
<li><code>first_published</code>: when the posting first went live.</li>
<li><code>updated_at</code>: the last time anything on it changed.</li>
</ul>
<p>On Discord's board all 44 jobs had an <code>updated_at</code> in the past week, but only 1 had a <code>first_published</code> in it (the oldest went live on 2025-10-30). Use <code>first_published</code> for "new jobs this week". <code>updated_at</code> makes everything look fresh.</p>
<h3>A wrong token</h3>
<pre><code>$ curl -s -w "\n%{http_code}\n" https://boards-api.greenhouse.io/v1/boards/notarealcompany12345/jobs
{"status":404,"error":"Job not found"}
404
</code></pre>
<p>The message says "Job not found" even though it's the board that's missing. A wrong job id on a real board (<code>/v1/boards/discord/jobs/1</code>) gives the identical 404 body, so you can't tell the two apart from the response. Check the token with the plain list first.</p>
<h2>3. Descriptions: ?content=true</h2>
<pre><code class="language-bash">curl "https://boards-api.greenhouse.io/v1/boards/discord/jobs?content=true"
</code></pre>
<p>This adds <code>content</code> (the job description), <code>departments</code> and <code>offices</code> to each job, plus the <code>ai_disclaimer</code>, <code>include_ai_disclaimer</code> and <code>ai_opt_out_request_url</code> fields. It's much heavier: Stripe's board went from 452 KB to 5.6 MB.</p>
<p>The description is HTML that has been HTML-escaped a second time. The raw value starts like this:</p>
<pre><code>&amp;lt;div class=&amp;quot;content-intro&amp;quot;&amp;gt;&amp;lt;p&amp;gt;Discord has a highly engaged community of millions ...
</code></pre>
<p>Unescape it once and you get the real markup:</p>
<pre><code class="language-python">import html, json, urllib.request

url = "https://boards-api.greenhouse.io/v1/boards/discord/jobs?content=true"
jobs = json.load(urllib.request.urlopen(url))["jobs"]
description_html = html.unescape(jobs[0]["content"])
# '&lt;div class="content-intro"&gt;&lt;p&gt;Discord has a highly engaged community ...'
</code></pre>
<p>If you render <code>content</code> without unescaping it, the reader sees literal tags.</p>
<h2>4. One job: GET /v1/boards/{token}/jobs/{id}</h2>
<pre><code class="language-bash">curl "https://boards-api.greenhouse.io/v1/boards/discord/jobs/8806482002"
</code></pre>
<p>This returns the same fields as a job in the <code>?content=true</code> list, including <code>content</code>, <code>departments</code> and <code>offices</code>. Use it when you already have the id, for example from a <code>gh_jid</code>.</p>
<h3>Salary: ?pay_transparency=true</h3>
<p>Salary ranges are off by default. Add <code>pay_transparency=true</code> and jobs gain a <code>pay_input_ranges</code> array:</p>
<pre><code class="language-bash">curl "https://boards-api.greenhouse.io/v1/boards/discord/jobs/8806482002?pay_transparency=true"
</code></pre>
<pre><code class="language-json">"pay_input_ranges": [
  {
    "min_cents": 19200000,
    "max_cents": 21600000,
    "currency_type": "USD",
    "title": "Base Salary",
    "blurb": "&lt;p&gt;&lt;em&gt;The base salary range for this full-time position is listed below. ..."
  }
]
</code></pre>
<p>That's $192,000 to $216,000, in cents. The parameter works on the list too: on Discord's board 41 of 44 jobs had a non-empty range. It only shows what the company entered, though. Stripe's 725 jobs all came back with no range.</p>
<h3>Application form: ?questions=true</h3>
<pre><code class="language-bash">curl "https://boards-api.greenhouse.io/v1/boards/discord/jobs/8806482002?questions=true"
</code></pre>
<p>This adds the application form: <code>questions</code>, <code>location_questions</code>, <code>compliance</code> (EEOC questions such as veteran status) and <code>demographic_questions</code>. The Discord job had 13 <code>questions</code>, starting with First Name, Last Name, Preferred First Name, Email, Phone, Resume/CV and Cover Letter, then company questions such as "Why do you want to work at Discord?". Each has a <code>label</code>, <code>required</code> flag and <code>fields</code> with a <code>name</code> and <code>type</code> (<code>input_text</code>, <code>input_file</code>, <code>textarea</code>, <code>multi_value_single_select</code>...). Resume/CV accepts either <code>input_file</code> or <code>textarea</code>.</p>
<h2>5. Group jobs: /departments and /offices</h2>
<pre><code class="language-bash">curl "https://boards-api.greenhouse.io/v1/boards/discord/departments"
curl "https://boards-api.greenhouse.io/v1/boards/discord/offices"
</code></pre>
<p><code>/departments</code> returns <code>{"departments": [...]}</code>. Each department has <code>id</code>, <code>name</code>, <code>parent_id</code>, <code>child_ids</code> and its <code>jobs</code>. Discord has 58 departments, and only 15 had open jobs: Product Engineering 12, Developers 6, Data Science &amp; Engineering 6, Product Design 5, the rest fewer. <code>parent_id</code> and <code>child_ids</code> give you the hierarchy, so you can roll jobs up to a top-level group.</p>
<p><code>/offices</code> returns <code>{"offices": [...]}</code>, each with <code>id</code>, <code>name</code>, <code>location</code>, <code>parent_id</code>, <code>child_ids</code> and a <code>departments</code> tree with jobs inside. Discord has 14 offices. Counting jobs per office: San Francisco 33, Remote (US) 10, and one each in Lisbon, Los Angeles, New York and Washington, D.C. That sums to 47 for 44 jobs, because a job can be in more than one office. Count unique job ids if you need totals.</p>
<p>Office names are whatever the company typed. Discord's list includes <code>Workday_Test</code> and <code>No Office</code>, and <code>location</code> can be <code>null</code> (<code>Canada</code>, <code>Remote (US)</code>). There is also no remote flag anywhere in the API. "Remote" only appears in office and location text.</p>
<h2>6. The application endpoint: leave it alone</h2>
<p>The same URL accepts a POST, which is how a careers page submits an application. Without credentials:</p>
<pre><code>$ curl -s -X POST -w "\n%{http_code}\n" https://boards-api.greenhouse.io/v1/boards/discord/jobs/8806482002
HTTP Basic: Access denied.
401
</code></pre>
<p>Submitting needs the company's own Job Board API key, which only the company has. Stick to the read side.</p>
<h2>Summary</h2>
<table>
<thead>
<tr>
<th>Call</th>
<th>Adds</th>
</tr>
</thead>
<tbody><tr>
<td><code>GET /v1/boards/{token}/jobs</code></td>
<td>every open job, no pagination</td>
</tr>
<tr>
<td><code>...?content=true</code></td>
<td>escaped HTML description, departments, offices</td>
</tr>
<tr>
<td><code>...?pay_transparency=true</code></td>
<td><code>pay_input_ranges</code>, where the company publishes them</td>
</tr>
<tr>
<td><code>GET /v1/boards/{token}/jobs/{id}</code></td>
<td>one job, with description</td>
</tr>
<tr>
<td><code>.../jobs/{id}?questions=true</code></td>
<td>the application form</td>
</tr>
<tr>
<td><code>GET /v1/boards/{token}/departments</code></td>
<td>department tree with jobs</td>
</tr>
<tr>
<td><code>GET /v1/boards/{token}/offices</code></td>
<td>office tree with departments and jobs</td>
</tr>
</tbody></table>
<h2>If you need more than Greenhouse</h2>
<p>One Greenhouse client is an afternoon's work. If you're watching companies across several ATSs, I maintain an Apify Actor, <a href="https://apify.com/jordan-belfort/ats-jobs-api">Company Jobs API</a>, that reads Greenhouse plus Workday, Lever, Ashby, Workable, SmartRecruiters, Recruitee and Personio and returns all of them in one schema. You can pass a careers URL like <code>https://boards.greenhouse.io/stripe</code> or just a company name or domain, and it finds the board. It costs $0.001 per job returned; jobs your filters drop and companies it can't find are free. Turn on <em>Only new jobs</em>, schedule it daily, and each run returns only postings that appeared since the last one.</p>
]]></content:encoded></item><item><title><![CDATA[Workday job postings API: how to pull every job from a myworkdayjobs.com careers site (with curl)]]></title><description><![CDATA[Tenant, host and site from any careers URL, the jobs endpoint, the 2,000-posting cap, facets and exact posting dates]]></description><link>https://jobdata.hashnode.dev/workday-job-postings-api-how-to-pull-every-job-from-a-myworkdayjobs-com-careers-site-with-curl</link><guid isPermaLink="true">https://jobdata.hashnode.dev/workday-job-postings-api-how-to-pull-every-job-from-a-myworkdayjobs-com-careers-site-with-curl</guid><category><![CDATA[api]]></category><category><![CDATA[webdev]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Jordan Belfort]]></dc:creator><pubDate>Wed, 21 Oct 2026 22:19:21 GMT</pubDate><content:encoded><![CDATA[<p>Workday doesn't publish a jobs API. But every <code>*.myworkdayjobs.com</code> careers site is a single-page app that loads its listings from a JSON endpoint, and you can call that endpoint yourself. This is a walkthrough of that endpoint using NVIDIA's careers site, with the calls I ran on 22 October 2026 and what came back, including the four places it quietly misbehaves.</p>
<p>It's the careers page's own backend, not a feed built for you, so treat it gently: read the tenant's robots.txt, keep one or two requests in flight, and cache what you fetch.</p>
<h2>1. Read tenant, host and site from the careers URL</h2>
<p>A Workday careers URL carries three things you need:</p>
<pre><code>https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite
        ^^^^^^ ^^^                    ^^^^^^^^^^^^^^^^^^^^^^^^
        tenant host (data centre)     site
</code></pre>
<ul>
<li><strong>tenant</strong>: the first label of the hostname (<code>nvidia</code>).</li>
<li><strong>host</strong>: <code>wd1</code>, <code>wd3</code>, <code>wd5</code>, <code>wd12</code>... It's the Workday data centre the company lives in. You can't guess it, so start from a real careers link.</li>
<li><strong>site</strong>: the first path segment (<code>NVIDIAExternalCareerSite</code>). One tenant can have several sites (internal, university, a recruiting-ads site).</li>
</ul>
<p>Many links carry a locale first: <code>https://salesforce.wd12.myworkdayjobs.com/en-US/External_Career_Site</code>. The locale is not the site. If you take the first segment blindly, the API tells you so:</p>
<pre><code>POST https://salesforce.wd12.myworkdayjobs.com/wday/cxs/salesforce/en-US/jobs
-&gt; 404 {"errorCode":"S21", ..., "message":"not found: Job_Posting_Site_ID=en-US"}
</code></pre>
<p>Skip a leading segment that looks like a locale (<code>xx-XX</code> or <code>xx</code>) and take the next one: <code>External_Career_Site</code>.</p>
<h2>2. List jobs: POST /wday/cxs/{tenant}/{site}/jobs</h2>
<pre><code class="language-bash">curl -X POST "https://nvidia.wd5.myworkdayjobs.com/wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"appliedFacets":{},"limit":20,"offset":0,"searchText":""}'
</code></pre>
<p>The response has four keys: <code>total</code>, <code>jobPostings</code>, <code>facets</code> and <code>userAuthenticated</code>. A posting in the list is thin:</p>
<pre><code class="language-json">{
  "title": "Senior Cell Modeling and Verification Engineer",
  "externalPath": "/job/US-CA-Santa-Clara/Senior-Cell-Modeling-and-Verification-Engineer_JR2015659",
  "locationsText": "US, CA, Santa Clara",
  "postedOn": "Posted Today",
  "bulletFields": ["JR2015659"]
}
</code></pre>
<p>The body fields:</p>
<ul>
<li><code>limit</code>: page size, <strong>20 at most</strong>. <code>"limit":21</code> got <code>400 {"errorCode":"HTTP_400", ...}</code> with an empty message.</li>
<li><code>offset</code>: where the page starts.</li>
<li><code>searchText</code>: the careers page's search box. <code>"CUDA"</code> gave <code>total: 530</code>.</li>
<li><code>appliedFacets</code>: filters, covered in section 4.</li>
</ul>
<p>The headers matter more than they look. Both errors come back with no message:</p>
<table>
<thead>
<tr>
<th>What I sent</th>
<th>Status</th>
</tr>
</thead>
<tbody><tr>
<td><code>Content-Type: application/json</code> + <code>Accept: application/json</code></td>
<td>200</td>
</tr>
<tr>
<td><code>Content-Type: application/json</code>, curl's default <code>Accept: */*</code></td>
<td>200</td>
</tr>
<tr>
<td><code>Content-Type: application/json</code> + <code>Accept: text/html</code></td>
<td>406</td>
</tr>
<tr>
<td>no <code>Content-Type</code> (curl sends form-encoded)</td>
<td>500</td>
</tr>
</tbody></table>
<p>So set both headers explicitly. An HTTP client that defaults to <code>Accept: text/html</code>, or one that posts form data, fails in ways that look like an outage.</p>
<h2>3. Paginate: <code>total</code> is only on page 1, and the list stops at 2,000</h2>
<p>Page one said <code>"total": 2000</code>. Page two (<code>offset: 20</code>) said <code>"total": 0</code>, with 20 postings in it. Read <code>total</code> from the first response and keep it; don't loop "while total &gt; offset" on every page.</p>
<p>The 2,000 is not NVIDIA's real job count. It's a cap. The <code>facets</code> on the same response count the jobs per category, and those add up to more: the <code>timeType</code> facet alone says Full time 2,713 + Part time 2. Workday serves at most 2,000 postings per list.</p>
<p>And past the cap it doesn't return an empty page. <code>offset: 1980</code> returned the last 20 (all "Posted 30+ Days Ago"). <code>offset: 2000</code> returned <strong>page one again</strong>, starting with the same "Senior Cell Modeling and Verification Engineer". A loop that stops on an empty page never stops; one that stops at <code>total</code> is fine, as long as <code>total</code> came from page one.</p>
<h2>4. Split a big tenant with facets</h2>
<p>To get past 2,000, ask for smaller lists. Each entry in <code>facets</code> has a <code>facetParameter</code> and <code>values</code> with an <code>id</code>, a <code>descriptor</code> and a <code>count</code>. NVIDIA's first page returned:</p>
<table>
<thead>
<tr>
<th>facetParameter</th>
<th>label</th>
<th>example values (count)</th>
</tr>
</thead>
<tbody><tr>
<td><code>jobFamilyGroup</code></td>
<td>Job Category</td>
<td>Engineering (1724), Sales (331), Univ Employment (184), Operations (110)... 15 values</td>
</tr>
<tr>
<td><code>workerSubType</code></td>
<td>Job Type</td>
<td>Regular Employee (2263), Management (198), Intern (Fixed Term) (182)...</td>
</tr>
<tr>
<td><code>timeType</code></td>
<td>Time Type</td>
<td>Full time (2713), Part time (2)</td>
</tr>
<tr>
<td><code>locationMainGroup</code></td>
<td>(nested)</td>
<td><code>locationHierarchy2</code> Office/Remote, <code>locationHierarchy1</code> 35 countries, <code>locations</code> 126 sites</td>
</tr>
</tbody></table>
<p><code>locationMainGroup</code> is a group: the filterable parameters are the nested ones (<code>locationHierarchy1</code> for countries, <code>locations</code> for sites). Facet names vary by tenant; Salesforce's site also had a custom one, <code>CF_-_REC_-_LRV_-_Job_Posting_Anchor_-_Country_from_Job_Posting_Location_Extended</code>. Read them from the response rather than hard-coding them.</p>
<p>Filter by passing the value ids under their parameter:</p>
<pre><code class="language-bash">curl -X POST "https://nvidia.wd5.myworkdayjobs.com/wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs" \
  -H "Content-Type: application/json" -H "Accept: application/json" \
  -d '{"appliedFacets":{"jobFamilyGroup":["0c40f6bd1d8f10ae43ffaefd46dc7e78"]},"limit":20,"offset":0,"searchText":""}'
</code></pre>
<p>That's Engineering, and it returned <code>"total": 1724</code>, the same number the facet promised, and under the cap. Sales returned 331. So the recipe is: read the facet counts from page one; for every value under 2,000, page through it; for any value still over 2,000, add a second facet (country inside Engineering, say). Jobs can sit in several location values, so dedupe on <code>externalPath</code> or the requisition id in <code>bulletFields</code>.</p>
<h2>5. Exact dates come from the detail call</h2>
<p><code>postedOn</code> is relative text: "Posted Today", "Posted 3 Days Ago", and for anything older, "Posted 30+ Days Ago". You can turn the first two into a date, but the last one only tells you "at least 30 days".</p>
<p>The detail endpoint is the same base plus the posting's <code>externalPath</code>, as a GET:</p>
<pre><code class="language-bash">curl "https://nvidia.wd5.myworkdayjobs.com/wday/cxs/nvidia/NVIDIAExternalCareerSite/job/UK-Remote/Developer-Relations-Manager--Physical-AI-Healthcare---EMEA_JR2021575" \
  -H "Accept: application/json"
</code></pre>
<p>Its <code>jobPostingInfo</code> said <code>"postedOn": "Posted 30+ Days Ago"</code> and <code>"startDate": "2026-07-30"</code>: almost three months, not one. The newer job above had <code>"startDate": "2026-10-21"</code>. The detail object also has <code>jobDescription</code> (HTML), <code>timeType</code>, <code>jobReqId</code>, <code>location</code>, <code>country</code> (with <code>alpha2Code</code> under <code>jobRequisitionLocation</code>), <code>canApply</code> and <code>externalUrl</code>, the public link to send people to. I saw no salary field.</p>
<p>That's one request per job, so for a 2,700-job tenant it's the expensive part. Fetch details only for postings you haven't seen before.</p>
<h2>6. Be polite</h2>
<p>NVIDIA's <code>robots.txt</code> allows <code>/NVIDIAExternalCareerSite/</code> and disallows <code>/NVIDIAExternalCareerSiteResearch/</code>, <code>/talentcommunity/</code>, <code>/NVIDIARecruitAds/</code> and <code>/refreshFacet/</code>. Check it per tenant and per site before you crawl. The same file points to a sitemap, <code>/NVIDIAExternalCareerSite/siteMap.xml</code>. Keep a few requests in flight per host, back off on errors, and don't re-fetch details for jobs you already have.</p>
<h2>If you need this for many companies</h2>
<p>Everything above works with curl. If you'd rather not maintain it, I built the <a href="https://apify.com/jordan-belfort/ats-jobs-api">Company Jobs API</a> on Apify. You give it Workday careers URLs (with or without a locale like <code>/en-US/</code>), and it returns one row per job in the same schema it uses for seven other ATSs: Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee and Personio. It costs $0.001 per job returned. With "Only new jobs" on and a daily schedule, each run returns just the postings that appeared since the last one. For Workday it keeps to the site's robots.txt and at most 4 requests at a time per company. It doesn't split by facets yet: for employers over the cap you get the newest 2,000, and the run log says so. Exact dates for "30+ days" jobs come from the detail call, made only for jobs that pass your filters.</p>
]]></content:encoded></item><item><title><![CDATA[How to pull job postings from Greenhouse, Lever, Ashby and Workday (the public APIs, with curl)]]></title><description><![CDATA[Most companies don't build their own careers page. It sits on an applicant tracking system (ATS), and several of the big ones publish every open job…]]></description><link>https://jobdata.hashnode.dev/how-to-pull-job-postings-from-greenhouse-lever-ashby-and-workday-the-public-apis-with-curl</link><guid isPermaLink="true">https://jobdata.hashnode.dev/how-to-pull-job-postings-from-greenhouse-lever-ashby-and-workday-the-public-apis-with-curl</guid><category><![CDATA[api]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Jordan Belfort]]></dc:creator><pubDate>Wed, 21 Oct 2026 19:39:52 GMT</pubDate><content:encoded><![CDATA[<p>Most companies don't build their own careers page. It sits on an applicant tracking system (ATS), and several of the big ones publish every open job through a public endpoint that needs no API key. This is the same data the company's own careers page loads. Here are the endpoints for Greenhouse, Lever, Ashby and Workday, with the curl calls I ran on 21 October 2026, how each one paginates, and what it answers when a company doesn't exist.</p>
<p>These feeds are meant to be read. Be polite: cache results, don't hammer them, and back off on a 429.</p>
<h2>Step zero: find the company's board slug</h2>
<p>The slug is the last path segment (or the subdomain) of the careers page, and you find it by following the "Apply" link on any job:</p>
<table>
<thead>
<tr>
<th>Careers page looks like</th>
<th>ATS</th>
<th>Slug</th>
</tr>
</thead>
<tbody><tr>
<td><code>boards.greenhouse.io/stripe</code> or <code>job-boards.greenhouse.io/stripe</code></td>
<td>Greenhouse</td>
<td><code>stripe</code></td>
</tr>
<tr>
<td><code>jobs.lever.co/spotify</code></td>
<td>Lever</td>
<td><code>spotify</code></td>
</tr>
<tr>
<td><code>jobs.ashbyhq.com/notion</code></td>
<td>Ashby</td>
<td><code>notion</code></td>
</tr>
<tr>
<td><code>pfizer.wd1.myworkdayjobs.com/PfizerCareers</code></td>
<td>Workday</td>
<td>tenant <code>pfizer</code>, host <code>wd1</code>, site <code>PfizerCareers</code></td>
</tr>
</tbody></table>
<p>Companies that serve their board from their own domain (Stripe's jobs link is <code>stripe.com/jobs/search?gh_jid=8172510</code>) can still be on one of these systems: the <code>absolute_url</code> in Stripe's Greenhouse response is exactly that link. When there is no giveaway, try the company name as a slug against each API below. Greenhouse, Lever and Ashby all answer 404 for an unknown company, so a 200 tells you which ATS it is.</p>
<h2>Greenhouse</h2>
<pre><code class="language-bash">curl "https://boards-api.greenhouse.io/v1/boards/stripe/jobs?content=true"
</code></pre>
<ul>
<li>Everything comes back in one response, with no pagination. Stripe returned 729 jobs, and <code>meta.total</code> says 729.</li>
<li>Top-level keys are <code>jobs</code> and <code>meta</code>. Each job has <code>id</code>, <code>title</code>, <code>location.name</code>, <code>absolute_url</code>, <code>first_published</code>, <code>updated_at</code>, <code>departments</code>, <code>offices</code> and <code>company_name</code>.</li>
<li><code>content=true</code> adds the description. It is HTML that has been HTML-escaped (<code>&amp;lt;h2&amp;gt;...</code>), so unescape it before you render it. For Stripe the response is 455 KB without it and 5.7 MB with it, so leave it off if you only need titles.</li>
<li>One job: <code>GET /v1/boards/stripe/jobs/8172510</code>. Board name: <code>GET /v1/boards/stripe</code>, which returned <code>{"name":"Stripe","content":""}</code>.</li>
<li>There is no remote flag. Remote only shows up in the location text.</li>
<li>Salary: my earlier notes say it appears in <code>pay_input_ranges</code> (<code>min_cents</code>, <code>max_cents</code>, <code>currency_type</code>) when a company publishes it. None of Stripe's 729 jobs had that field today, so I can't show real output for it.</li>
<li>Unknown board: HTTP 404 with <code>{"status":404,"error":"Job not found"}</code>.</li>
</ul>
<h2>Lever</h2>
<pre><code class="language-bash">curl "https://api.lever.co/v0/postings/spotify?mode=json"
curl "https://api.lever.co/v0/postings/spotify?mode=json&amp;skip=0&amp;limit=5"
</code></pre>
<ul>
<li>Without <code>limit</code> you get a bare JSON array of everything (Spotify: 80 postings). With <code>skip</code> and <code>limit</code> you page through it: <code>limit=5</code> returned the first five of the same list.</li>
<li>Fields: <code>id</code>, <code>text</code> (the title), <code>categories</code> (<code>commitment</code>, <code>department</code>, <code>location</code>, <code>team</code>, <code>allLocations</code>), <code>country</code>, <code>workplaceType</code> (<code>hybrid</code> on the first Spotify posting), <code>hostedUrl</code>, <code>applyUrl</code>, <code>descriptionPlain</code>, <code>description</code>, <code>lists</code> and <code>additional</code>.</li>
<li><code>createdAt</code> is a Unix timestamp in milliseconds, not an ISO string. Spotify's first posting had <code>1791566108174</code>, which is 2026-10-09.</li>
<li>There are two regions. Companies hosted at <code>jobs.eu.lever.co</code> live on <code>api.eu.lever.co</code> and answer 404 on the US host, so try both before giving up.</li>
<li>Salary, where published, is in <code>salaryRange</code>. None of Spotify's postings had it today.</li>
<li>Unknown company: HTTP 404 with <code>{"ok":false,"error":"Document not found"}</code>.</li>
</ul>
<h2>Ashby</h2>
<pre><code class="language-bash">curl "https://api.ashbyhq.com/posting-api/job-board/notion?includeCompensation=true"
</code></pre>
<ul>
<li>One response, no pagination: <code>{"apiVersion": ..., "jobs": [...]}</code>. Notion had 137 jobs.</li>
<li>Fields: <code>id</code>, <code>title</code>, <code>department</code>, <code>team</code>, <code>employmentType</code> (<code>FullTime</code>), <code>location</code>, <code>secondaryLocations</code>, <code>publishedAt</code>, <code>isListed</code>, <code>isRemote</code>, <code>workplaceType</code>, <code>address</code>, <code>jobUrl</code>, <code>applyUrl</code>, <code>descriptionHtml</code> and <code>descriptionPlain</code>.</li>
<li>Remote is not clean. In Notion's data, 86 jobs had <code>isRemote: true</code> together with <code>workplaceType: "Hybrid"</code>, and 51 had both set to <code>null</code>. Don't treat <code>isRemote</code> as a reliable filter. Read <code>workplaceType</code> and the location text as well.</li>
<li><code>includeCompensation=true</code> adds a <code>compensation</code> object with <code>compensationTierSummary</code>, <code>compensationTiers</code> and <code>summaryComponents</code>. Notion's were all empty today, so check for that before you read a salary.</li>
<li>Unknown company: HTTP 404 with the body <code>Not Found</code>.</li>
</ul>
<h2>Workday</h2>
<p>Workday has no public jobs API. Every <code>*.myworkdayjobs.com</code> careers site loads its listings from a JSON endpoint that the page itself calls, and that is what you read here. It is the careers page's own backend, not a published feed, so check the tenant's robots.txt and keep only a few requests in flight.</p>
<p>Three parts of the URL come from the careers page and none of them can be guessed: the tenant (<code>pfizer</code>), the data-centre host (<code>wd1</code>, <code>wd5</code>, <code>wd12</code>...) and the site name (<code>PfizerCareers</code>). Drop any locale segment such as <code>/en-US/</code>.</p>
<pre><code class="language-bash">curl -X POST "https://pfizer.wd1.myworkdayjobs.com/wday/cxs/pfizer/PfizerCareers/jobs" \
  -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"appliedFacets":{},"limit":20,"offset":0,"searchText":""}'
</code></pre>
<p>What I saw today:</p>
<ul>
<li>The response has <code>total</code> (560 for Pfizer), <code>jobPostings</code>, <code>facets</code> and <code>userAuthenticated</code>. A posting has <code>title</code>, <code>externalPath</code>, <code>locationsText</code>, <code>postedOn</code> ("Posted 12 Days Ago") and <code>bulletFields</code> (the requisition id).</li>
<li><code>limit</code> above 20 is rejected. I sent 21 and got HTTP 400.</li>
<li><code>total</code> is only filled in on the first page. At <code>offset: 20</code> it came back as <code>0</code> while still returning 20 jobs, so read it once and don't trust it after that.</li>
<li><code>Content-Type: application/json</code> is required: without it I got HTTP 500. The <code>Accept</code> header was not required in my test (HTTP 200 without it), but I send it anyway.</li>
<li>Search works server-side: <code>"searchText":"data engineer"</code> with <code>limit: 5</code> returned <code>total: 80</code>.</li>
<li>A tenant that doesn't exist answered HTTP 422 for me, not 404, so check the status code and the presence of <code>jobPostings</code>.</li>
<li>A site serves at most 2,000 postings. Past that, offsets hand back the first page again, so de-duplicate on <code>externalPath</code>. I saw this on an earlier test (18 October); Pfizer has too few jobs to show it today.</li>
<li>The list has no real date or description: older jobs just say "Posted 30+ Days Ago". For those, GET the detail:</li>
</ul>
<pre><code class="language-bash">curl "https://pfizer.wd1.myworkdayjobs.com/wday/cxs/pfizer/PfizerCareers/job/United-States---New-York---Pearl-River/Facilities-Engineer---Electrical-and-Controls_4965638-1" \
  -H "Accept: application/json"
</code></pre>
<p>That is <code>.../wday/cxs/&lt;tenant&gt;/&lt;site&gt;</code> plus the posting's <code>externalPath</code>. The <code>jobPostingInfo</code> object has <code>jobDescription</code> (HTML), <code>startDate</code> (<code>2026-10-09</code> for that job), <code>timeType</code> ("Full time"), <code>jobReqId</code>, <code>location</code>, <code>country</code> and <code>endDate</code>. There is no salary field. The public job page is <code>https://pfizer.wd1.myworkdayjobs.com/PfizerCareers</code> plus the <code>externalPath</code>.</p>
<h2>The other four, quickly</h2>
<p>All calls below were run today.</p>
<table>
<thead>
<tr>
<th>ATS</th>
<th>Call</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>Workable</td>
<td><code>GET https://apply.workable.com/api/v1/widget/accounts/huggingface?details=true</code></td>
<td><code>name</code>, <code>description</code> and <code>jobs[]</code> (6 jobs), with <code>telecommuting</code> as the remote flag. 404 for unknown</td>
</tr>
<tr>
<td>SmartRecruiters</td>
<td><code>GET https://api.smartrecruiters.com/v1/companies/BoschGroup/postings?limit=100&amp;offset=0</code></td>
<td><code>totalFound: 5100</code> and <code>content[]</code>. <code>limit</code> max 100. An unknown company returns <strong>200 with <code>totalFound: 0</code></strong>, not 404, and the id is case-sensitive. The list has no descriptions: each one is another call to <code>/postings/&lt;id&gt;</code></td>
</tr>
<tr>
<td>Recruitee</td>
<td><code>GET https://bunq.recruitee.com/api/offers/</code></td>
<td><code>offers[]</code> (20), with <code>remote</code>, <code>hybrid</code> and <code>on_site</code> booleans, <code>salary</code> and <code>careers_apply_url</code>. 404 for unknown</td>
</tr>
<tr>
<td>Personio</td>
<td><code>GET https://personio.jobs.personio.de/xml?language=en</code></td>
<td>XML, root <code>&lt;workzag-jobs&gt;</code>, one <code>&lt;position&gt;</code> per job (<code>id</code>, <code>name</code>, <code>office</code>, <code>department</code>, <code>employmentType</code>, <code>schedule</code>, <code>createdAt</code>). Unknown company: 404 with an HTML page</td>
</tr>
</tbody></table>
<h2>If you'd rather not maintain eight parsers</h2>
<p>The gotchas add up: several pagination styles, a millisecond timestamp, a 200-for-nothing, a Workday host you have to know in advance. If you only need one company, the curl calls above are enough.</p>
<p>If you want many companies in one schema, I built the <a href="https://apify.com/jordan-belfort/ats-jobs-api">Company Jobs API</a> on Apify. You give it careers page URLs, domains or company names, it finds the ATS (Workday needs the full careers URL), and it returns one row per job with the same fields whatever the source. It covers the eight ATSs in this article. It costs $0.001 per job returned; jobs your filters drop and companies it can't find are free. It also has a monitor mode: schedule it daily with "Only new jobs" on, and each run returns just the postings that appeared since the last one. You don't need it to use anything above.</p>
<p><em>I'm Jordan Belfort, and I build open job-data tools. If an endpoint here has changed, tell me and I'll fix the article.</em></p>
]]></content:encoded></item></channel></rss>