<?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: Make No Mistakes LLC</title>
    <description>The latest articles on DEV Community by Make No Mistakes LLC (@makenomistakesllc).</description>
    <link>https://dev.to/makenomistakesllc</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%2F4125070%2F54eec67e-cdfb-4bb9-b296-310a609cf663.png</url>
      <title>DEV Community: Make No Mistakes LLC</title>
      <link>https://dev.to/makenomistakesllc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/makenomistakesllc"/>
    <language>en</language>
    <item>
      <title>Tech careers pages are usually one of 9 public ATS APIs. Here they are on one schema.</title>
      <dc:creator>Make No Mistakes LLC</dc:creator>
      <pubDate>Tue, 15 Sep 2026 04:50:14 +0000</pubDate>
      <link>https://dev.to/makenomistakesllc/tech-careers-pages-are-usually-one-of-9-public-ats-apis-here-they-are-on-one-schema-28pe</link>
      <guid>https://dev.to/makenomistakesllc/tech-careers-pages-are-usually-one-of-9-public-ats-apis-here-they-are-on-one-schema-28pe</guid>
      <description>&lt;p&gt;Open a tech company's careers page with devtools on. More often than not the page is a thin shell and&lt;br&gt;
every job on it arrives in one JSON response from a third-party applicant tracking system —&lt;br&gt;
Greenhouse, Ashby, Lever, Workday, SmartRecruiters, Recruitee, Rippling, Workable or Personio.&lt;/p&gt;

&lt;p&gt;Those responses come from public, unauthenticated endpoints the ATS vendors publish so aggregators&lt;br&gt;
can consume them. No key, no login, no CAPTCHA, no headless browser. If you have been writing HTML&lt;br&gt;
parsers against careers pages, you have been parsing the render of a document you could have fetched&lt;br&gt;
directly.&lt;/p&gt;
&lt;h2&gt;
  
  
  One request, one whole board
&lt;/h2&gt;

&lt;p&gt;Greenhouse is the simplest of the nine. One GET returns every open posting for a company, with&lt;br&gt;
descriptions, and there is no pagination at all:&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;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;

&lt;span class="c1"&gt;# No API key, no auth header. This is the documented public board endpoint.
&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://boards-api.greenhouse.io/v1/boards/stripe/jobs?content=true&amp;amp;pay_transparency=true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&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;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;User-Agent&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;job-board-reader/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;board&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&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;board&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobs&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;open roles&lt;/span&gt;&lt;span class="sh"&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;job&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;dept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&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;departments&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="mi"&gt;0&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;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&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;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&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="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="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&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;name&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;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dept&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&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="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;absolute_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;stripe&lt;/code&gt; for any Greenhouse board token — often visible in the careers page URL, or in that&lt;br&gt;
network request you just watched. The rest of this article is the eight variations on it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The nine endpoints
&lt;/h2&gt;

&lt;p&gt;Every endpoint below is one this project calls in production, verified live before shipping. The&lt;br&gt;
query parameters shown are the ones that matter, not the full surface.&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;Endpoint&lt;/th&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Greenhouse&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true&amp;amp;pay_transparency=true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whole board, one response, descriptions included. Documented at &lt;code&gt;docs.greenhouse.io/job-board.html&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;code&gt;GET api.ashbyhq.com/posting-api/job-board/{org}?includeCompensation=true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whole board, one response.&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;code&gt;GET api.lever.co/v0/postings/{org}?mode=json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whole board, returned as a bare JSON &lt;strong&gt;array&lt;/strong&gt;, not an object.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SmartRecruiters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET api.smartrecruiters.com/v1/companies/{co}/postings?limit=100&amp;amp;offset=N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Offset-paginated, 100/page. List carries &lt;strong&gt;no description&lt;/strong&gt; — that needs &lt;code&gt;…/postings/{id}&lt;/code&gt; per job.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Recruitee&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET {co}.recruitee.com/api/offers/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whole board, one response.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rippling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET api.rippling.com/platform/api/ats/v1/board/{co}/jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Thin list — uuid, name, department, url, work location. Everything else needs &lt;code&gt;…/jobs/{uuid}&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Personio&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET {co}.jobs.personio.de/xml&lt;/code&gt; (also &lt;code&gt;.personio.com&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;The only &lt;strong&gt;XML&lt;/strong&gt; feed of the nine.&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;code&gt;POST {tenant}.wd{N}.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;POST, 20 per page, and the list rows are near-useless. See below.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Workable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET apply.workable.com/api/v1/widget/accounts/{co}?details=true&lt;/code&gt; + &lt;code&gt;POST apply.workable.com/api/v3/accounts/{co}/jobs&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Two endpoints together: the widget has descriptions, v3 is authoritative on the total and fills in employment type and the remote flag.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Six of the nine hand you a usable board in one request. Rippling returns the same &lt;code&gt;uuid&lt;/code&gt; once per&lt;br&gt;
work location, so sixteen board rows legitimately collapsing to seven jobs is normal. And Workday is&lt;br&gt;
a category of its own.&lt;/p&gt;
&lt;h2&gt;
  
  
  Workday is the one that will cost you a weekend
&lt;/h2&gt;

&lt;p&gt;Every Workday employer lives on its own host — &lt;code&gt;nvidia.wd5.myworkdayjobs.com&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;salesforce.wd12.myworkdayjobs.com&lt;/code&gt; — where the pod (&lt;code&gt;wd1&lt;/code&gt; through &lt;code&gt;wd12&lt;/code&gt; cover most tenants, with a few higher-numbered pods) is&lt;br&gt;
assigned, not chosen, under a &lt;strong&gt;site name the employer picked&lt;/strong&gt;: &lt;code&gt;External&lt;/code&gt;, &lt;code&gt;NVIDIAExternalCareerSite&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;Salesforce_Careers&lt;/code&gt;. You need both before you can make a single request, and Workday publishes a&lt;br&gt;
directory of neither.&lt;/p&gt;

&lt;p&gt;The trick that works is &lt;code&gt;GET https://{tenant}.wd{N}.myworkdayjobs.com/robots.txt&lt;/code&gt; — Workday serves&lt;br&gt;
the real site name there, in the &lt;code&gt;Sitemap:&lt;/code&gt; line and the &lt;code&gt;Allow:&lt;/code&gt; rules, so one cheap request per&lt;br&gt;
pod resolves almost every tenant. Failing that, POST the jobs endpoint with common site names and&lt;br&gt;
read the status codes: in our testing a wrong &lt;em&gt;pod&lt;/em&gt; answers 422 while the right pod with a wrong&lt;br&gt;
&lt;em&gt;site&lt;/em&gt; answers 404, so a 404 confirms the host. None of that is contract. Treat Workday as the part&lt;br&gt;
of your pipeline most likely to break, and note that its public feed carries no department, no team&lt;br&gt;
and no salary at all.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the nine actually disagree
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Salary
&lt;/h3&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;Structured pay range?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ashby&lt;/td&gt;
&lt;td&gt;✅ Full block — min, max, currency, interval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;✅ When the employer opts into pay transparency; values arrive &lt;strong&gt;in cents&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lever&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;salaryRange&lt;/code&gt; when the employer fills it in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recruitee / Rippling&lt;/td&gt;
&lt;td&gt;⚠️ Fields exist, rarely populated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SmartRecruiters, Workable, Personio, Workday&lt;/td&gt;
&lt;td&gt;❌ Nothing public&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Having the field is not the same as the field being filled. On a live sample of 40 jobs per board&lt;br&gt;
taken 2026-09-02, Ashby boards published a range on 100% of Ramp's postings and 90% of OpenAI's;&lt;br&gt;
Greenhouse was 22% (GitLab) and 0% (Stripe); Lever was 50% (Match Group) and 0% (Palantir). If you&lt;br&gt;
need compensation data, that is the whole answer: &lt;strong&gt;Ashby, then hope.&lt;/strong&gt; And do not regex the&lt;br&gt;
description for a dollar figure — a guessed number in a &lt;code&gt;salary_min&lt;/code&gt; column is worse than an honest&lt;br&gt;
&lt;code&gt;null&lt;/code&gt;, because it looks like data.&lt;/p&gt;
&lt;h3&gt;
  
  
  Departments and dates
&lt;/h3&gt;

&lt;p&gt;Greenhouse gives department only; Ashby, Lever and Rippling give department &lt;em&gt;and&lt;/em&gt; team; Workday&lt;br&gt;
gives neither.&lt;/p&gt;

&lt;p&gt;Only &lt;strong&gt;three&lt;/strong&gt; of the nine publish an updated timestamp — Greenhouse, SmartRecruiters and Recruitee.&lt;br&gt;
That matters more than it sounds: if you want "what changed on these boards this week," on the other&lt;br&gt;
six you can detect &lt;em&gt;new&lt;/em&gt; postings and nothing else. Formats vary too; Lever's &lt;code&gt;createdAt&lt;/code&gt; is epoch&lt;br&gt;
milliseconds.&lt;/p&gt;
&lt;h3&gt;
  
  
  "Not found" is not always 404
&lt;/h3&gt;

&lt;p&gt;This is where scrapers silently report empty boards:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SmartRecruiters&lt;/strong&gt;: an unknown company answers &lt;strong&gt;200&lt;/strong&gt; with &lt;code&gt;totalFound: 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workable&lt;/strong&gt;: an unknown account answers &lt;strong&gt;404&lt;/strong&gt;, but a real account with no live jobs answers
&lt;strong&gt;200&lt;/strong&gt; with an empty list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workday&lt;/strong&gt;: wrong pod → 422; wrong site on the right pod → 404 (observed, not documented).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personio and Workable&lt;/strong&gt; both rate-limit by IP and answer &lt;strong&gt;429&lt;/strong&gt; under even light parallel
probing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is dangerous. An unhandled 429 looks exactly like "this company has zero open jobs,"&lt;br&gt;
which downstream reads as "every role was closed." Pace those two, honour &lt;code&gt;Retry-After&lt;/code&gt;, and never&lt;br&gt;
let a non-200 collapse into an empty result.&lt;/p&gt;
&lt;h2&gt;
  
  
  The lookup nobody ships
&lt;/h2&gt;

&lt;p&gt;Knowing the endpoints is half of it. The other half: given "Match Group," which of the nine is it&lt;br&gt;
on, under what slug? There is no registry. What works is generating plausible slugs — the domain&lt;br&gt;
label, the name lowercased and de-spaced, the hyphenated variant, the variant with &lt;code&gt;Inc&lt;/code&gt; / &lt;code&gt;GmbH&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;Group&lt;/code&gt; stripped — and probing them cheapest-first: the six single-request platforms in parallel,&lt;br&gt;
then Personio and Workable paced because of the 429s, then Workday last.&lt;/p&gt;

&lt;p&gt;On a mixed, deliberately tech-skewed set of 20 companies we tested (Stripe, OpenAI, Anthropic,&lt;br&gt;
Databricks, Ramp, Notion, Linear, Datadog, NVIDIA, Vercel, Figma, Airtable, Brex, Cloudflare, Match&lt;br&gt;
Group, Vandebron, Adverity and scale.com among them) this resolved &lt;strong&gt;18&lt;/strong&gt;. The two misses, DoorDash&lt;br&gt;
and Retool, run career sites that are not on any of the nine public APIs — there is nothing to find.&lt;/p&gt;

&lt;p&gt;Two failure modes to know before you build it yourself. &lt;strong&gt;A slug can belong to someone else&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;scale.com&lt;/code&gt; resolves to a six-job Personio board owned by a different company called Scale, while&lt;br&gt;
Scale AI's real board is on Greenhouse under &lt;code&gt;scaleai&lt;/code&gt;, and nothing in either response distinguishes&lt;br&gt;
them. And &lt;strong&gt;companies mid-migration are live on two platforms at once&lt;/strong&gt; with the same roles on both,&lt;br&gt;
so de-duplication cannot key on the platform's job id — it has to fall back to title and location.&lt;br&gt;
If you only need a handful of known companies, hardcode the board URLs: a board URL short-circuits&lt;br&gt;
discovery entirely and can never re-resolve onto someone else's board.&lt;/p&gt;
&lt;h2&gt;
  
  
  One schema across all nine
&lt;/h2&gt;

&lt;p&gt;24 flat fields, one item per posting, and anything a platform does not publish is &lt;code&gt;null&lt;/code&gt; rather than&lt;br&gt;
absent — so the output exports to CSV or a database without ragged columns:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;company&lt;/code&gt;, &lt;code&gt;company_slug&lt;/code&gt;, &lt;code&gt;ats&lt;/code&gt;, &lt;code&gt;job_id&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;department&lt;/code&gt;, &lt;code&gt;team&lt;/code&gt;, &lt;code&gt;location_raw&lt;/code&gt;, &lt;code&gt;city&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;state&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;is_remote&lt;/code&gt;, &lt;code&gt;employment_type&lt;/code&gt;, &lt;code&gt;salary_min&lt;/code&gt;, &lt;code&gt;salary_max&lt;/code&gt;, &lt;code&gt;salary_currency&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;salary_period&lt;/code&gt;, &lt;code&gt;posted_at&lt;/code&gt;, &lt;code&gt;updated_at&lt;/code&gt;, &lt;code&gt;apply_url&lt;/code&gt;, &lt;code&gt;description_html&lt;/code&gt;, &lt;code&gt;description_text&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;source_url&lt;/code&gt;, &lt;code&gt;raw&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rules that made that work across nine wildly different payloads:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;raw&lt;/code&gt; keeps the untouched platform record.&lt;/strong&gt; Normalization is lossy by definition; &lt;code&gt;raw&lt;/code&gt; is the
escape hatch, so nothing an ATS publishes is destroyed by the mapping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dates are dates.&lt;/strong&gt; &lt;code&gt;YYYY-MM-DD&lt;/code&gt; everywhere — epoch milliseconds, ISO strings and "Posted Today"
all land in the same format. &lt;code&gt;employment_type&lt;/code&gt; collapses to &lt;code&gt;FULL_TIME&lt;/code&gt; / &lt;code&gt;PART_TIME&lt;/code&gt; /
&lt;code&gt;CONTRACT&lt;/code&gt; / &lt;code&gt;TEMPORARY&lt;/code&gt; / &lt;code&gt;INTERNSHIP&lt;/code&gt; / &lt;code&gt;VOLUNTEER&lt;/code&gt;, &lt;code&gt;salary_period&lt;/code&gt; to &lt;code&gt;YEAR&lt;/code&gt; … &lt;code&gt;HOUR&lt;/code&gt;, and
Greenhouse's cents get divided by 100 on the way in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;is_remote&lt;/code&gt;&lt;/strong&gt; is true when the platform sets a flag, or the location string says remote /
anywhere / distributed / work-from-home. Titles and descriptions are never consulted — they say
"remote" for reasons that have nothing to do with the role.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;location_raw&lt;/code&gt; is the truth; &lt;code&gt;city&lt;/code&gt;/&lt;code&gt;state&lt;/code&gt;/&lt;code&gt;country&lt;/code&gt; are best-effort.&lt;/strong&gt; Only Ashby,
SmartRecruiters, Recruitee and Workable publish a structured address; the rest is free text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;company&lt;/code&gt; is exact only where the platform publishes it&lt;/strong&gt; — six of the nine do. Ashby, Lever and
Workday do not, so there it is derived from your input: &lt;code&gt;"Match Group"&lt;/code&gt; stays &lt;code&gt;"Match Group"&lt;/code&gt;, but
a bare slug like &lt;code&gt;openai&lt;/code&gt; becomes &lt;code&gt;"Openai"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The limitation that shapes everything
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ATS endpoints expose only currently-open roles.&lt;/strong&gt; None of the nine carries history. A job that&lt;br&gt;
closed yesterday is simply gone — no tombstone, no closed-at date, no way to backfill it from any of&lt;br&gt;
these endpoints, ever.&lt;/p&gt;

&lt;p&gt;That decides the architecture of anything built on this data. If you care about what &lt;em&gt;changed&lt;/em&gt;, the&lt;br&gt;
only way to get it is to snapshot boards on a schedule and diff, starting the day you decide you&lt;br&gt;
care — every day not snapshotted is history permanently lost. It is also why the 429 handling&lt;br&gt;
matters: a rate-limited fetch that reads as "zero jobs" makes a naive differ publish &lt;em&gt;every role at&lt;br&gt;
that company closed&lt;/em&gt;. A board that verified at N jobs and returns 0 today is a suspect fetch, not a&lt;br&gt;
mass layoff, and that guard belongs in the pipeline before the first "closed" event is ever written.&lt;br&gt;
Internally we run exactly this — a daily full snapshot of a 200-company watchlist, diffed into&lt;br&gt;
opened/closed events. The fetching is easy; the guards are the work.&lt;/p&gt;
&lt;h2&gt;
  
  
  If you don't want to maintain this
&lt;/h2&gt;

&lt;p&gt;Nine adapters, a discovery layer and a normalizer are not hard to write, but they are an ongoing&lt;br&gt;
obligation to keep alive — Workday's &lt;code&gt;robots.txt&lt;/code&gt; behaviour in particular owes you nothing. It is&lt;br&gt;
packaged as an Apify Actor:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://apify.com/make_no_mistakes/multi-ats-job-board-api" rel="noopener noreferrer"&gt;https://apify.com/make_no_mistakes/multi-ats-job-board-api&lt;/a&gt;&lt;/strong&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;requests&lt;/span&gt;

&lt;span class="n"&gt;ACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;make_no_mistakes~multi-ats-job-board-api&lt;/span&gt;&lt;span class="sh"&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://api.apify.com/v2/acts/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACTOR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/run-sync-get-dataset-items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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;params&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;token&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;YOUR_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;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;companies&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;stripe&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;openai.com&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;Match Group&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;maxItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;titleIncludes&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;engineer&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;postedWithinDays&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&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;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;posted_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;j&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="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;salary_min&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apply_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pay-per-event: &lt;strong&gt;$0.005&lt;/strong&gt; to start a run, &lt;strong&gt;$0.004&lt;/strong&gt; per job written to the dataset, or &lt;strong&gt;$0.001&lt;/strong&gt;&lt;br&gt;
per company in &lt;code&gt;discoverOnly&lt;/code&gt; mode, which answers "which ATS, which board, how many roles open"&lt;br&gt;
without paying for the jobs. Resolving a 200-company watchlist that way costs $0.205; pulling 500&lt;br&gt;
jobs costs $2.005. Filtered-out jobs are never written and never billed.&lt;/p&gt;

&lt;p&gt;The limitations, in one place: salary is thin outside Ashby; Personio and Workable rate-limit by IP,&lt;br&gt;
so a watchlist leaning on those two can see individual companies come back unresolved; Workday&lt;br&gt;
discovery is undocumented behaviour that can stop working without notice (the other eight are&lt;br&gt;
unaffected and the run does not fail); there is no history, only open roles; and employers that&lt;br&gt;
aren't on any of the nine — DoorDash and Retool, for two — can't be found, because there is nothing&lt;br&gt;
to fetch.&lt;/p&gt;

&lt;p&gt;One more, because it bites in production: every pay-per-event run carries a charge ceiling&lt;br&gt;
(&lt;code&gt;maxTotalChargeUsd&lt;/code&gt;, set from your remaining credit unless you set it yourself), and at $0.004 a&lt;br&gt;
job that is a maximum item count. Pack too many companies into one run and it stops there — and&lt;br&gt;
says so: the companies it never reached are named in &lt;code&gt;RUN_SUMMARY.companies_not_fetched&lt;/code&gt;, given an&lt;br&gt;
entry each in &lt;code&gt;RUN_SUMMARY.errors&lt;/code&gt;, and deliberately left out of the per-company counts rather than&lt;br&gt;
written as &lt;code&gt;0&lt;/code&gt;, because &lt;code&gt;0&lt;/code&gt; means "this board is empty" and that is a different fact.&lt;/p&gt;

&lt;p&gt;Or skip all of it and go straight to the nine endpoints in the table above. They are public, the&lt;br&gt;
vendors publish them for exactly this purpose, and for a fixed list of companies you already know,&lt;br&gt;
hardcoding the adapters is quick work. The only thing being sold here is the discovery layer, the&lt;br&gt;
normalization, and the obligation to keep both alive. Either way the employer is the system of&lt;br&gt;
record — these endpoints return what companies published about themselves, and their terms, and the&lt;br&gt;
ATS vendor's, are worth reading before you redistribute any of it.&lt;/p&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>11 US cities publish building permits as open data. Here they are on one schema.</title>
      <dc:creator>Make No Mistakes LLC</dc:creator>
      <pubDate>Mon, 14 Sep 2026 19:38:04 +0000</pubDate>
      <link>https://dev.to/makenomistakesllc/11-us-cities-publish-building-permits-as-open-data-here-they-are-on-one-schema-3ahp</link>
      <guid>https://dev.to/makenomistakesllc/11-us-cities-publish-building-permits-as-open-data-here-they-are-on-one-schema-3ahp</guid>
      <description>&lt;p&gt;Every construction project in the United States starts the same way: somebody pulls a permit. That&lt;br&gt;
permit becomes a public record on the day it is issued, and most large cities publish it as open data&lt;br&gt;
within a day or two — the address, what is being built, who is building it, and what it is worth.&lt;/p&gt;

&lt;p&gt;It is one of the most useful public datasets in the country, and it is almost unusable in aggregate,&lt;br&gt;
because every city publishes it differently.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why permits matter
&lt;/h2&gt;

&lt;p&gt;A permit is the earliest broadly available signal that something is about to physically change at an&lt;br&gt;
address. It precedes the dumpster, the crew, the listing, the sale comp and the assessor's revaluation.&lt;/p&gt;

&lt;p&gt;Concretely, that makes permit data useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contractor lead generation.&lt;/strong&gt; A new roof permit is a buying signal for gutters, solar and siding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Materials and equipment sales.&lt;/strong&gt; Declared valuation tells a supply rep roughly how big a job is
before the framing goes up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-estate research.&lt;/strong&gt; Permit velocity by ZIP shows where capital is landing, months ahead of
sales comps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitive analysis.&lt;/strong&gt; Grouping by contractor name shows who is winning work, by trade and by
neighborhood.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this requires anything private. It is all public record. The problem is purely mechanical.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fragmentation problem
&lt;/h2&gt;

&lt;p&gt;There is no national permit feed. There is no shared schema. There is not even a shared platform.&lt;br&gt;
Across the eleven cities covered here you get three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Socrata (SODA 2.1)&lt;/strong&gt; — eight cities. Query with &lt;code&gt;$where&lt;/code&gt;, &lt;code&gt;$order&lt;/code&gt;, &lt;code&gt;$limit&lt;/code&gt;, &lt;code&gt;$offset&lt;/code&gt;. Pages cap
at 1,000 rows for anonymous callers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CKAN datastore&lt;/strong&gt; — two cities (Boston, San Antonio). Query with &lt;code&gt;datastore_search_sql&lt;/code&gt;, which
takes actual SQL with &lt;code&gt;LIMIT&lt;/code&gt;/&lt;code&gt;OFFSET&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Carto SQL API&lt;/strong&gt; — one city (Philadelphia), at &lt;code&gt;phl.carto.com/api/v2/sql&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, inside each platform, the field names drift. Take the single most important column — the date a&lt;br&gt;
permit was issued, which is what any date filter has to run against:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column name&lt;/th&gt;
&lt;th&gt;Cities&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;issue_date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Austin, Chicago, Los Angeles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;issued_date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;New York, San Francisco&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;issueddate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Seattle, Cincinnati, Baton Rouge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"DATE ISSUED"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;San Antonio (quoted, uppercase, with a space)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The drift also happens over time, within one city. Austin's dataset used to expose &lt;code&gt;permit_num&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;issued_date&lt;/code&gt;; it now exposes &lt;code&gt;permit_number&lt;/code&gt; and &lt;code&gt;issue_date&lt;/code&gt;. That kind of rename is worse than an&lt;br&gt;
outage, because a query against the old names does not error — it returns nothing, and a nightly job&lt;br&gt;
quietly starts writing zero rows.&lt;/p&gt;

&lt;p&gt;Multiply that across permit number, permit type, permit class, work description, status, address,&lt;br&gt;
ZIP, latitude, longitude, contractor, valuation and owner, and "just query the open data" turns into&lt;br&gt;
a per-city mapping problem.&lt;/p&gt;
&lt;h2&gt;
  
  
  The eleven cities
&lt;/h2&gt;

&lt;p&gt;All eleven feeds below were verified live before publishing, and all eleven refresh daily. Lag is the&lt;br&gt;
gap between a permit being issued and it appearing in the feed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;City&lt;/th&gt;
&lt;th&gt;Source portal&lt;/th&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Lag&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Austin, TX&lt;/td&gt;
&lt;td&gt;data.austintexas.gov — Issued Construction Permits (&lt;code&gt;3syk-w9eu&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Socrata&lt;/td&gt;
&lt;td&gt;same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;San Antonio, TX&lt;/td&gt;
&lt;td&gt;data.sanantonio.gov — Permits Issued&lt;/td&gt;
&lt;td&gt;CKAN&lt;/td&gt;
&lt;td&gt;2–4 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New York, NY&lt;/td&gt;
&lt;td&gt;data.cityofnewyork.us — DOB NOW: Build Approved Permits (&lt;code&gt;rbx6-tga4&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Socrata&lt;/td&gt;
&lt;td&gt;same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Los Angeles, CA&lt;/td&gt;
&lt;td&gt;data.lacity.org — LADBS Building + Electrical + Mechanical&lt;/td&gt;
&lt;td&gt;Socrata&lt;/td&gt;
&lt;td&gt;1–2 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chicago, IL&lt;/td&gt;
&lt;td&gt;data.cityofchicago.org — Building Permits (&lt;code&gt;ydr8-5enu&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Socrata&lt;/td&gt;
&lt;td&gt;same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Philadelphia, PA&lt;/td&gt;
&lt;td&gt;OpenDataPhilly — L&amp;amp;I Building Permits&lt;/td&gt;
&lt;td&gt;Carto&lt;/td&gt;
&lt;td&gt;same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boston, MA&lt;/td&gt;
&lt;td&gt;data.boston.gov — Approved Building Permits&lt;/td&gt;
&lt;td&gt;CKAN&lt;/td&gt;
&lt;td&gt;same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seattle, WA&lt;/td&gt;
&lt;td&gt;data.seattle.gov — Building Permits (&lt;code&gt;76t5-zqzr&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Socrata&lt;/td&gt;
&lt;td&gt;1–3 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;San Francisco, CA&lt;/td&gt;
&lt;td&gt;data.sf.gov — Building Permits (&lt;code&gt;i98e-djp9&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Socrata&lt;/td&gt;
&lt;td&gt;same day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cincinnati, OH&lt;/td&gt;
&lt;td&gt;data.cincinnati-oh.gov — Building Permits (&lt;code&gt;uhjb-xac9&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Socrata&lt;/td&gt;
&lt;td&gt;1–3 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baton Rouge, LA&lt;/td&gt;
&lt;td&gt;data.brla.gov — Permits Issued (&lt;code&gt;7fq7-8j7r&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Socrata&lt;/td&gt;
&lt;td&gt;1–3 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of these need an asterisk. Los Angeles is three separate LADBS datasets merged into one city.&lt;br&gt;
San Antonio's feed is a general permits register, so administrative items (garage sale permits, tree&lt;br&gt;
affidavits) sit next to construction work, and its work-description column is really the city's&lt;br&gt;
PROJECT NAME field, which on small permits is often just the address repeated.&lt;/p&gt;
&lt;h2&gt;
  
  
  The normalized schema
&lt;/h2&gt;

&lt;p&gt;Eighteen flat fields, one item per permit. Anything a city does not publish is &lt;code&gt;null&lt;/code&gt; rather than&lt;br&gt;
absent, so the output exports to CSV or a database without ragged columns. A real record:&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;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Austin, TX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permit_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1995-014830 BP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permit_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Building Permit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permit_class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Residential"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"work_description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add Bedroom &amp;amp; Level Portion Of Exist Foundation"&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;"Active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issued_date"&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-31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applied_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1995-05-08"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4907 SHOAL CREEK BLVD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"zip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"78756"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;30.3230315&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lng"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-97.74450439&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contractor_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Butlin Homes, Inc."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contractor_phone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5127732944"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valuation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;11000.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;"owner_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://abc.austintexas.gov/web/permit/public-search-other?t_detail=1&amp;amp;t_selected_folderrsn=629045"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"raw"&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;"permittype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"work_class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Addition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tcad_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0227000414"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"..."&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;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;raw&lt;/code&gt; carries the original city record, minus geometry blobs and internal index columns, so nothing&lt;br&gt;
the city publishes is lost to the normalization.&lt;/p&gt;

&lt;p&gt;The important caveat is that &lt;strong&gt;field coverage is a property of the city, not of the normalizer&lt;/strong&gt;.&lt;br&gt;
Austin is the only one of the eleven that publishes contractor phone numbers, at roughly 92% fill.&lt;br&gt;
New York, Los Angeles, Boston, Seattle, San Francisco, Cincinnati and Baton Rouge fill valuation at&lt;br&gt;
or near 100%; Austin fills it at 14%. Owner name exists in New York, Chicago, Philadelphia and Baton&lt;br&gt;
Rouge and mostly does not elsewhere. No amount of schema work creates a column a city never wrote.&lt;/p&gt;
&lt;h2&gt;
  
  
  The metros that are missing, and why
&lt;/h2&gt;

&lt;p&gt;Dallas, Houston, Phoenix, Denver, Miami, Fort Worth, Nashville and Mesa are all absent. Every one of&lt;br&gt;
them has a permit dataset you can find in a search engine. Every one of those datasets is&lt;br&gt;
discontinued, frozen years in the past, or served only as an ArcGIS layer that no longer responds.&lt;/p&gt;

&lt;p&gt;A city that silently returns zero rows is worse than a city that is openly missing, because it turns&lt;br&gt;
into an invisible gap in someone's analysis. They stay out until their feeds come back.&lt;/p&gt;

&lt;p&gt;If you are evaluating a municipal open-data feed yourself, the check takes ten seconds: sort&lt;br&gt;
descending on the date column and look at the newest row. If it is from 2019, the dataset is a museum&lt;br&gt;
piece regardless of what the portal page says.&lt;/p&gt;
&lt;h2&gt;
  
  
  Use it
&lt;/h2&gt;

&lt;p&gt;The eleven cities are packaged as an Apify Actor:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://apify.com/make_no_mistakes/us-building-permits-scraper" rel="noopener noreferrer"&gt;https://apify.com/make_no_mistakes/us-building-permits-scraper&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pick cities, pick a window, optionally filter by keyword. Priced per record returned.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/make_no_mistakes~us-building-permits-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"cities":["austin"],"daysBack":7,"maxItems":200}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;requests&lt;/span&gt;

&lt;span class="n"&gt;ACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;make_no_mistakes~us-building-permits-scraper&lt;/span&gt;&lt;span class="sh"&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://api.apify.com/v2/acts/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACTOR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/run-sync-get-dataset-items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;permits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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;params&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;token&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;YOUR_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;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;cities&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;austin&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;boston&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;daysBack&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permitTypes&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;roof, solar&lt;/span&gt;&lt;span class="sh"&gt;"&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;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;permits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issued_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contractor_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;work_description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run-sync-get-dataset-items&lt;/code&gt; blocks until the run finishes and hands back the dataset as JSON, which&lt;br&gt;
is the right shape for a one-off pull or a daily cron. For anything larger, start the run&lt;br&gt;
asynchronously and read the dataset when it finishes.&lt;/p&gt;

&lt;p&gt;Or skip all of it and go straight to the portals in the table above. The data is public records and&lt;br&gt;
the cities are the system of record — the only thing being sold here is the eleven mappings and the&lt;br&gt;
work of keeping them alive.&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>api</category>
      <category>python</category>
      <category>data</category>
    </item>
  </channel>
</rss>
