<?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: Juan Carlos Gutierrez</title>
    <description>The latest articles on DEV Community by Juan Carlos Gutierrez (@juancarlosguti).</description>
    <link>https://dev.to/juancarlosguti</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%2F4084046%2F664a3d8c-2bb4-49b8-91c4-7367a2271f62.jpg</url>
      <title>DEV Community: Juan Carlos Gutierrez</title>
      <link>https://dev.to/juancarlosguti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/juancarlosguti"/>
    <language>en</language>
    <item>
      <title>Ask Workday's public API for 100 jobs and you get zero — with no error</title>
      <dc:creator>Juan Carlos Gutierrez</dc:creator>
      <pubDate>Wed, 19 Aug 2026 02:12:04 +0000</pubDate>
      <link>https://dev.to/juancarlosguti/ask-workdays-public-api-for-100-jobs-and-you-get-zero-with-no-error-54b2</link>
      <guid>https://dev.to/juancarlosguti/ask-workdays-public-api-for-100-jobs-and-you-get-zero-with-no-error-54b2</guid>
      <description>&lt;p&gt;Every major applicant tracking system publishes its job board as a public JSON&lt;br&gt;
endpoint. That part is already well documented, and I'm not going to rewrite it.&lt;/p&gt;

&lt;p&gt;What isn't documented is what happens once you actually try to read all of them&lt;br&gt;
and put the results in one table. I built a normalizer across seven ATS —&lt;br&gt;
Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters and Recruitee — and&lt;br&gt;
every single interesting bug came from an API that answered &lt;code&gt;200 OK&lt;/code&gt; while&lt;br&gt;
telling me something false.&lt;/p&gt;

&lt;p&gt;These are the five that cost me real time. All of them are verifiable with&lt;br&gt;
&lt;code&gt;curl&lt;/code&gt;, no authentication, no account.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Workday silently caps &lt;code&gt;limit&lt;/code&gt; at 20 — and returns an empty array if you exceed it
&lt;/h2&gt;

&lt;p&gt;Workday's job search is a POST, not a GET:&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://${host}/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;The obvious optimization is to raise &lt;code&gt;limit&lt;/code&gt; to cut the number of round trips.&lt;br&gt;
Try it:&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="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"appliedFacets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&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="nl"&gt;"offset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"searchText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get &lt;code&gt;200 OK&lt;/code&gt;. You get a well-formed response body. And &lt;code&gt;jobPostings&lt;/code&gt; is an&lt;br&gt;
&lt;strong&gt;empty array&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No error. No warning. No &lt;code&gt;400&lt;/code&gt;. Nothing that tells you the request was invalid.&lt;/p&gt;

&lt;p&gt;This is the worst possible failure mode, because a naive implementation doesn't&lt;br&gt;
crash — it concludes &lt;em&gt;"this company has no open roles"&lt;/em&gt; and moves on. If you're&lt;br&gt;
running across hundreds of tenants, you will silently emit zero rows for every&lt;br&gt;
Workday employer and the run will look perfectly healthy.&lt;/p&gt;

&lt;p&gt;The real ceiling is 20. Hardcode it and paginate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Workday's real ceiling. Higher values return an empty array.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I lost an afternoon to this before I thought to compare &lt;code&gt;limit: 20&lt;/code&gt; against&lt;br&gt;
&lt;code&gt;limit: 100&lt;/code&gt; on the same tenant.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Workday stops paginating at 10,000 postings
&lt;/h2&gt;

&lt;p&gt;On large tenants, pagination stops returning new results once &lt;code&gt;offset&lt;/code&gt; passes&lt;br&gt;
10,000. There's no flag in the response telling you that you've hit a wall&lt;br&gt;
rather than the end of the list.&lt;/p&gt;

&lt;p&gt;If you don't cap explicitly, your loop either terminates on an empty page and&lt;br&gt;
under-reports, or spins. Cap it and record that you capped it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_POSTINGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The general principle, and the reason both of these bit me: &lt;strong&gt;an ATS telling you&lt;br&gt;
"no more results" and an ATS refusing to give you more results look identical&lt;br&gt;
over HTTP.&lt;/strong&gt; You have to decide which one you're looking at, and log it.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. There is no public directory of Workday tenants
&lt;/h2&gt;

&lt;p&gt;Greenhouse, Lever and Ashby all take a single slug you can guess from the&lt;br&gt;
company's domain. Workday needs three values — host, tenant and site:&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
        └─ host ──────────────────┘       └─ site ──────────────┘
              └ tenant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wd5&lt;/code&gt; isn't guessable (it's whichever Workday cluster the customer landed on),&lt;br&gt;
and the site name is free text the customer chose. There's no registry, no&lt;br&gt;
lookup API, nothing to enumerate.&lt;/p&gt;

&lt;p&gt;Practical consequence: for six of the seven ATS you can auto-discover a board&lt;br&gt;
from a company domain. For Workday you cannot — the only reliable source of&lt;br&gt;
those three values is the careers URL itself. So I let users paste the URL and I&lt;br&gt;
parse it, and I left Workday out of automatic discovery entirely rather than&lt;br&gt;
pretend it works.&lt;/p&gt;

&lt;p&gt;Being explicit about what &lt;em&gt;can't&lt;/em&gt; be automated turned out to be more useful than&lt;br&gt;
a discovery function that quietly misses every Workday employer.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Ashby has salary data, but only if you ask for it by name
&lt;/h2&gt;

&lt;p&gt;Ashby's public board endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://api.ashbyhq.com/posting-api/job-board/ramp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postings come back with &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, &lt;code&gt;employmentType&lt;/code&gt;, &lt;code&gt;descriptionHtml&lt;/code&gt;&lt;br&gt;
and friends. No compensation field anywhere — not &lt;code&gt;null&lt;/code&gt;, &lt;em&gt;absent&lt;/em&gt;. Easy to&lt;br&gt;
conclude Ashby doesn't expose salary.&lt;/p&gt;

&lt;p&gt;It does. You have to opt in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://api.ashbyhq.com/posting-api/job-board/ramp?includeCompensation=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every posting carries a &lt;code&gt;compensation&lt;/code&gt; object with tier summaries, currency&lt;br&gt;
codes and min/max values. On the board I tested, &lt;strong&gt;131 of 137 postings&lt;/strong&gt; had&lt;br&gt;
compensation data that the default response omits entirely.&lt;/p&gt;

&lt;p&gt;An absent field reads exactly like "this data doesn't exist." Here it meant "you&lt;br&gt;
didn't ask."&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Three of the seven never tell you the company's name
&lt;/h2&gt;

&lt;p&gt;This one I only caught by auditing my own output. I noticed 30% of my rows had&lt;br&gt;
&lt;code&gt;companyName: null&lt;/code&gt;, and the nulls weren't random — they were 100% of the rows&lt;br&gt;
from specific sources.&lt;/p&gt;

&lt;p&gt;Where the name comes from, per ATS:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ATS&lt;/th&gt;
&lt;th&gt;Company display name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET /v1/boards/{slug}&lt;/code&gt; → &lt;code&gt;name&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workable&lt;/td&gt;
&lt;td&gt;account endpoint → &lt;code&gt;name&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SmartRecruiters&lt;/td&gt;
&lt;td&gt;on each posting → &lt;code&gt;company.name&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recruitee&lt;/td&gt;
&lt;td&gt;on each offer → &lt;code&gt;company_name&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ashby&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;not exposed&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lever&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;not exposed&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Workday&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;not exposed&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ashby's board response has exactly two top-level keys — &lt;code&gt;jobs&lt;/code&gt; and &lt;code&gt;apiVersion&lt;/code&gt;.&lt;br&gt;
Lever returns a bare array of postings with no envelope at all. Neither carries&lt;br&gt;
the employer's display name in any field, at any level, including inside&lt;br&gt;
individual postings.&lt;/p&gt;

&lt;p&gt;Worth noting because my own code had a comment claiming Ashby exposed&lt;br&gt;
&lt;code&gt;organizationName&lt;/code&gt; "on some boards." It doesn't, on any board I could find. The&lt;br&gt;
comment was a guess someone (me) wrote once and never checked, and it survived&lt;br&gt;
because the field silently resolved to &lt;code&gt;null&lt;/code&gt; instead of throwing.&lt;/p&gt;

&lt;p&gt;If you need a name for those three, you derive it from the slug and you say so&lt;br&gt;
in your schema. Don't let a derived value sit in the same column as an&lt;br&gt;
authoritative one without marking the difference.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bonus trap: the company's domain is not the company's slug
&lt;/h2&gt;

&lt;p&gt;If you're auto-discovering boards from domains, the obvious approach — strip&lt;br&gt;
&lt;code&gt;www.&lt;/code&gt; and the TLD, use what's left — fails on a whole category of companies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;datadoghq.com&lt;/code&gt; → slug &lt;code&gt;datadog&lt;/code&gt; on Greenhouse&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;joinhandshake.com&lt;/code&gt; → slug &lt;code&gt;handshake&lt;/code&gt; on Ashby&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Companies register a padded domain because the clean one was taken, then use the&lt;br&gt;
clean name on their ATS. &lt;code&gt;datadoghq&lt;/code&gt; returns 404 on Greenhouse; &lt;code&gt;datadog&lt;/code&gt; returns&lt;br&gt;
424 open roles. Stripping a short list of common paddings (&lt;code&gt;hq&lt;/code&gt;, &lt;code&gt;inc&lt;/code&gt;, &lt;code&gt;app&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;labs&lt;/code&gt;, &lt;code&gt;hr&lt;/code&gt;) and prefixes (&lt;code&gt;join&lt;/code&gt;, &lt;code&gt;try&lt;/code&gt;) recovered a board I'd been silently&lt;br&gt;
missing for months.&lt;/p&gt;
&lt;h2&gt;
  
  
  The finding I didn't expect: salary coverage is a market fact, not a parsing problem
&lt;/h2&gt;

&lt;p&gt;I built currency and period normalization so an hourly US rate and a monthly&lt;br&gt;
Colombian salary sort in the same column. I was proud of it. Then I measured&lt;br&gt;
what fraction of postings actually carry any salary at all.&lt;/p&gt;

&lt;p&gt;On a sample of &lt;strong&gt;566 postings open to candidates in Latin America&lt;/strong&gt;: &lt;strong&gt;8.7%&lt;/strong&gt;&lt;br&gt;
had a parseable salary.&lt;/p&gt;

&lt;p&gt;By source it's sharper still:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ATS&lt;/th&gt;
&lt;th&gt;Share of rows&lt;/th&gt;
&lt;th&gt;Rows with salary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;td&gt;3.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ashby&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;td&gt;16.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lever&lt;/td&gt;
&lt;td&gt;7%&lt;/td&gt;
&lt;td&gt;35.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The source carrying 70% of the volume is the one that almost never has a number&lt;br&gt;
in it.&lt;/p&gt;

&lt;p&gt;The reason has nothing to do with the parser. US pay-transparency laws force&lt;br&gt;
employers to publish ranges; most Latin American postings simply don't include&lt;br&gt;
one. A parser can't extract a number that was never written.&lt;/p&gt;

&lt;p&gt;I'm including this because "salary parsed and annualized" is the kind of feature&lt;br&gt;
that sounds like a guarantee and isn't. If you build something similar, measure&lt;br&gt;
your fill rate per region before you put it in a headline. Mine is in the&lt;br&gt;
documentation now, with the number.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why every run should report per-source health
&lt;/h2&gt;

&lt;p&gt;The through-line of all of the above: these APIs fail by returning less, not by&lt;br&gt;
returning errors. A capped Workday page, a 404 on a renamed slug, and a genuinely&lt;br&gt;
quiet hiring week are indistinguishable if all you look at is the row count.&lt;/p&gt;

&lt;p&gt;So every run emits a status block before anything else:&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checkedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-18T23:02:22Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"greenhouse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"boards"&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="nl"&gt;"boardsOk"&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="nl"&gt;"jobsFound"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2817&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"jobsEmitted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;373&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ashby"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"boards"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"boardsOk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"jobsFound"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"jobsEmitted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ashby:ramp: HTTP 404 (board not found)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;jobsFound&lt;/code&gt; versus &lt;code&gt;jobsEmitted&lt;/code&gt; matters as much as the status: it separates&lt;br&gt;
"the source is broken" from "your filters are aggressive." A scheduled run&lt;br&gt;
against a fixed set of boards works as a canary — if an ATS changes its shape, I&lt;br&gt;
find out before a user does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything here is public, and that's the point
&lt;/h2&gt;

&lt;p&gt;All seven endpoints answer without a session, a cookie, or an account. That&lt;br&gt;
constraint isn't just convenience — it's the entire legal footing. Public data&lt;br&gt;
reachable without logging in sits on very different ground from data behind an&lt;br&gt;
authentication wall, and the difference has been expensive for companies that&lt;br&gt;
got it wrong.&lt;/p&gt;

&lt;p&gt;If a source needs a login to reach, it's out. There's plenty here without it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;A daily run of this publishes a free open dataset of LATAM-eligible postings —&lt;br&gt;
schema, caveats and both endpoints are documented at&lt;br&gt;
&lt;a href="https://github.com/JuanCarlosGuti/latam-tech-jobs" rel="noopener noreferrer"&gt;github.com/JuanCarlosGuti/latam-tech-jobs&lt;/a&gt;.&lt;br&gt;
No token, no signup. The normalizer itself I maintain as an&lt;br&gt;
&lt;a href="https://apify.com/crearcode/greenhouse-lever-ashby-jobs-api" rel="noopener noreferrer"&gt;Apify actor&lt;/a&gt;.&lt;br&gt;
Happy to answer questions about any of these endpoints.&lt;/em&gt;&lt;/p&gt;

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