<?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: Ben Barazandeh</title>
    <description>The latest articles on DEV Community by Ben Barazandeh (@dedicated_crest).</description>
    <link>https://dev.to/dedicated_crest</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%2F4207142%2F33dbbacd-8d9a-4b55-90a3-03ad7330b025.png</url>
      <title>DEV Community: Ben Barazandeh</title>
      <link>https://dev.to/dedicated_crest</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dedicated_crest"/>
    <language>en</language>
    <item>
      <title>Collecting App Store Reviews by Country and Version With Apple's Public Feed</title>
      <dc:creator>Ben Barazandeh</dc:creator>
      <pubDate>Fri, 30 Oct 2026 14:12:14 +0000</pubDate>
      <link>https://dev.to/dedicated_crest/collecting-app-store-reviews-by-country-and-version-with-apples-public-feed-2fod</link>
      <guid>https://dev.to/dedicated_crest/collecting-app-store-reviews-by-country-and-version-with-apples-public-feed-2fod</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. Everything in the tutorial part works with free tools too.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A star average tells you almost nothing. "4.6 stars" hides the fact that version 7.144.0 broke streak freezes for half your users in the UK. What product teams actually want is reviews &lt;strong&gt;by version and by country&lt;/strong&gt;, for their own app and for competitors.&lt;/p&gt;

&lt;p&gt;For your own app, App Store Connect has the reviews and lets you reply. For any other app, there's no official reviews API, but Apple does publish a public &lt;strong&gt;customer-reviews feed&lt;/strong&gt; per storefront, and the iTunes Search/Lookup API covers the app metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the feed works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://itunes.apple.com/{country}/rss/customerreviews/page={n}/id={appId}/sortby={mostrecent|mosthelpful}/json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;country&lt;/code&gt; is a storefront code (&lt;code&gt;us&lt;/code&gt;, &lt;code&gt;gb&lt;/code&gt;, &lt;code&gt;de&lt;/code&gt;, &lt;code&gt;jp&lt;/code&gt;, &lt;code&gt;br&lt;/code&gt;…). &lt;strong&gt;Every storefront has its own reviews.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Each page has up to 50 reviews, and you get &lt;strong&gt;at most 10 pages per sort order&lt;/strong&gt;. That's 500 for &lt;code&gt;mostrecent&lt;/code&gt; and 500 for &lt;code&gt;mosthelpful&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each entry has the rating, title, text, author, &lt;strong&gt;app version&lt;/strong&gt; and date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app ID is the number after &lt;code&gt;id&lt;/code&gt; in the App Store URL (&lt;code&gt;389801252&lt;/code&gt; for Instagram). If you only have a name, the iTunes Search API turns it into an ID, and Lookup gives you the average rating, rating count and current version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: A collector you can run today
&lt;/h2&gt;

&lt;p&gt;This pulls both sort orders for one storefront and dedupes on review ID. It ran as-is on 25 Oct 2026:&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mostrecent&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;mosthelpful&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;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# Apple serves at most 10 pages of 50 per sort order
&lt;/span&gt;            &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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://itunes.apple.com/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/rss/customerreviews/&lt;/span&gt;&lt;span class="sh"&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;page=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/id=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;app_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sortby=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;entries&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;get&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;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&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="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="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;feed&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="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;entry&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="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# defensive: normalise a lone object to a list
&lt;/span&gt;                &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entries&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;im:rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c1"&gt;# skip anything that isn't a review
&lt;/span&gt;                &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&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;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&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;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;im:rating&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;label&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;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;im:version&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;label&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;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;e&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;label&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;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;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated&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;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;389801252&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# Instagram, UK storefront
&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;rows&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unique reviews&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="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;are 1-2 stars&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;I ran it twice, a few minutes apart. The first run printed &lt;code&gt;749 unique reviews / 492 are 1-2 stars&lt;/code&gt;. The second printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;996 unique reviews
611 are 1-2 stars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same app, same storefront, same code. The difference is the feed's habit of returning an empty page before the real end: my loop treats an empty page as "done", so the first run stopped early on at least one sort order. That's the most important pitfall in this whole post, and the fix is to retry empty pages (with a fresh cache-buster) before giving up.&lt;/p&gt;

&lt;p&gt;Also notice the skew: the newest and "most helpful" reviews lean negative (611 of 996 here). That describes this sample, not every review ever written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The cap is per sort order and per country.&lt;/strong&gt; To see more, add storefronts. A US-only pull misses most of the feedback for an app that's big in Germany, Japan or Brazil.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History needs polling.&lt;/strong&gt; You only ever see roughly the newest 500. To build a full history, run on a schedule and keep what you've already stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty pages that aren't the end.&lt;/strong&gt; As above. Retry an empty page a couple of times with a new cache-busting parameter before you treat it as the last page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure.&lt;/strong&gt; Every value is wrapped in &lt;code&gt;{"label": ...}&lt;/code&gt;, and dates come with a UTC offset (e.g. &lt;code&gt;-07:00&lt;/code&gt;). Normalise to UTC before you compare countries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version is the useful field.&lt;/strong&gt; Group 1★ reviews by &lt;code&gt;im:version&lt;/code&gt; and release-related spikes jump out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Or use the packaged version
&lt;/h2&gt;

&lt;p&gt;I packaged this as an Apify Actor, &lt;a href="https://apify.com/dedicated_crest/app-store-reviews-scraper" rel="noopener noreferrer"&gt;App Store Reviews Scraper&lt;/a&gt;. It takes App Store URLs, numeric IDs or plain app names, any number of countries, and &lt;code&gt;sort&lt;/code&gt; set to most recent, most helpful or both (deduplicated, up to about 1,000 unique per app per country).&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;"apps"&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;"https://apps.apple.com/us/app/instagram/id389801252"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"duolingo"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"countries"&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;"us"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gb"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxReviewsPerApp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&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 can filter by min/max stars, "only reviews since" a date, and keywords in the title or text. Filtered-out reviews aren't charged. Optional app details go on every row: name, developer, bundle ID, genre, average rating, rating count, current version.&lt;/p&gt;

&lt;p&gt;That input returned 200 reviews from 4 app/country combinations in about 22 seconds on 18 Oct 2026. Some rows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App&lt;/th&gt;
&lt;th&gt;Store&lt;/th&gt;
&lt;th&gt;Rating&lt;/th&gt;
&lt;th&gt;Review title&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instagram&lt;/td&gt;
&lt;td&gt;GB&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;Like a digital scrap book&lt;/td&gt;
&lt;td&gt;451.0.0&lt;/td&gt;
&lt;td&gt;2026-10-17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instagram&lt;/td&gt;
&lt;td&gt;GB&lt;/td&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;td&gt;Suspended exile&lt;/td&gt;
&lt;td&gt;451.0.0&lt;/td&gt;
&lt;td&gt;2026-10-16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duolingo: Learn Languages&lt;/td&gt;
&lt;td&gt;US&lt;/td&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;td&gt;Annoying streak freezes&lt;/td&gt;
&lt;td&gt;7.144.0&lt;/td&gt;
&lt;td&gt;2026-10-17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duolingo: Language &amp;amp; Chess&lt;/td&gt;
&lt;td&gt;GB&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;So good&lt;/td&gt;
&lt;td&gt;7.144.0&lt;/td&gt;
&lt;td&gt;2026-10-17&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the name difference: the same Duolingo app has a different title in the US and UK storefronts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow I'd use
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Your app plus 3–5 competitors, in the 5–10 countries that matter to you.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxRating: 2&lt;/code&gt; for a complaints feed, or no filter for everything.&lt;/li&gt;
&lt;li&gt;A weekly schedule with "only reviews since" set to last week.&lt;/li&gt;
&lt;li&gt;Send the rows to a sheet, or to an LLM step that clusters complaints by version.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apple's cap is the cap.&lt;/strong&gt; About 500 per sort order per country, so around 1,000 per country at best. Anything older is only reachable if you started polling earlier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Google Play.&lt;/strong&gt; There's no official public reviews API for other people's apps on Play.&lt;/li&gt;
&lt;li&gt;Not affiliated with Apple.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;$0.0002 per review&lt;/strong&gt; ($0.20 per 1,000). App lookups and filtered-out reviews are free, there's no start fee, and runs stop at your "maximum cost per run". The 200-review test above came to 4 cents. Some Store Actors are slightly cheaper per review. The script above is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play fair
&lt;/h2&gt;

&lt;p&gt;Reviews are public, but they're written by people. Don't republish them with names attached, and follow Apple's terms and privacy laws.&lt;/p&gt;

&lt;p&gt;How do you analyse reviews today: by hand, with tags, or with an LLM? I'm curious what's actually worked for you, so tell me in the comments.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>python</category>
      <category>api</category>
      <category>productmanagement</category>
    </item>
    <item>
      <title>Finding Contact Emails on Company Websites Politely: robots.txt, mailto, JSON-LD and the Traps</title>
      <dc:creator>Ben Barazandeh</dc:creator>
      <pubDate>Thu, 29 Oct 2026 14:03:03 +0000</pubDate>
      <link>https://dev.to/dedicated_crest/finding-contact-emails-on-company-websites-politely-robotstxt-mailto-json-ld-and-the-traps-35l3</link>
      <guid>https://dev.to/dedicated_crest/finding-contact-emails-on-company-websites-politely-robotstxt-mailto-json-ld-and-the-traps-35l3</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. Everything in the tutorial part works with free tools too.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You have a list of company domains (a CRM export, a lead list, a spreadsheet from an event) and you need a contact email, a phone number and maybe the company's Instagram or LinkedIn. The details are usually right there on the company's own site. Getting them reliably, and politely, takes more care than a regex over the homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: A minimal, polite extractor
&lt;/h2&gt;

&lt;p&gt;This is roughly the smallest version I'd be comfortable running against small-business sites. It checks robots.txt with its own User-Agent, reads the homepage, pulls &lt;code&gt;mailto:&lt;/code&gt; and &lt;code&gt;tel:&lt;/code&gt; links, and lists same-domain contact/about pages to visit next. It ran as-is on 25 Oct 2026:&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;re&lt;/span&gt;&lt;span class="p"&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="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;robotparser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urljoin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;

&lt;span class="n"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MyContactBot/0.1 (+mailto:you@example.com)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&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;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="n"&gt;UA&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;15&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;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;geturl&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&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;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;rp&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;robotparser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RobotFileParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# fetch robots.txt with *your* UA; RobotFileParser.read() uses urllib's default
&lt;/span&gt;        &lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;urljoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/robots.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;  &lt;span class="c1"&gt;# no robots.txt (404) means allowed
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;can_fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;site&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;blocked_by_robots_txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;emails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mailto:([^&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\'?&amp;gt;\s]+)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;phones&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tel:([+\d()\-. ]{7,})&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;
    &lt;span class="n"&gt;contact_links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;urljoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&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;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;href=&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="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;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contact|about|impressum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                     &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;urljoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;site&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emails&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phones&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phones&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contactPages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contact_links&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;contacts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.aloha-dental.com/&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;Output:&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="err"&gt;'site':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'https://www.aloha-dental.com/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'emails':&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="err"&gt;'phones':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="mi"&gt;707-7300&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="err"&gt;'contactPages':&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'https://www.aloha-dental.com/about-us/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'https://www.aloha-dental.com/contact-us/'&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;Two lessons from that one run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, the homepage had no email.&lt;/strong&gt; The phone was there as a &lt;code&gt;tel:&lt;/code&gt; link, but the address lives deeper in the site. My packaged tool (below) found &lt;code&gt;riverside@aloha-dental.com&lt;/code&gt; on the same site after visiting 5 pages. You almost always need to follow the contact/about links.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, my first version wrongly reported this site as blocked by robots.txt.&lt;/strong&gt; Python's &lt;code&gt;RobotFileParser.read()&lt;/code&gt; fetches robots.txt with urllib's default User-Agent, the server answered 403, and the parser treats an access error as "disallow everything". Fetching robots.txt yourself with your real UA fixed it. If your crawler says "blocked" a lot, check that before blaming the sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The traps you'll hit next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Emails aren't only in &lt;code&gt;mailto:&lt;/code&gt;.&lt;/strong&gt; Look in page text, and in schema.org JSON-LD (&lt;code&gt;"email"&lt;/code&gt;, &lt;code&gt;"telephone"&lt;/code&gt;, &lt;code&gt;"address"&lt;/code&gt;), which a lot of small-business sites ship through their CMS or SEO plugin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Obfuscation.&lt;/strong&gt; Cloudflare's email protection replaces addresses with a &lt;code&gt;data-cfemail&lt;/code&gt; hex string (a simple XOR you can decode). People also write &lt;code&gt;name [at] domain [dot] com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Junk addresses.&lt;/strong&gt; Placeholder and tracking addresses (&lt;code&gt;example.com&lt;/code&gt;, Sentry DSNs, &lt;code&gt;wixpress&lt;/code&gt;) show up in page source. Filter them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dead domains.&lt;/strong&gt; An email whose domain has no MX record won't receive mail. A DNS lookup is cheap and doesn't touch anyone's mailbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phone false positives.&lt;/strong&gt; Order numbers, dates and prices look like phone numbers in text. Trust &lt;code&gt;tel:&lt;/code&gt; links and schema.org more than regexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No contact link at all.&lt;/strong&gt; Fall back to trying &lt;code&gt;/contact&lt;/code&gt;, &lt;code&gt;/contact-us&lt;/code&gt; and &lt;code&gt;/about&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be slow.&lt;/strong&gt; These are often tiny shared-hosting sites. One request per second per site, an identifiable User-Agent, and robots.txt &lt;code&gt;Crawl-delay&lt;/code&gt; if they set one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Or use the packaged version
&lt;/h2&gt;

&lt;p&gt;I packaged this into an Apify Actor, &lt;a href="https://apify.com/dedicated_crest/website-contact-enrichment" rel="noopener noreferrer"&gt;Website Contact Scraper&lt;/a&gt;. You paste domains, URLs or emails, and get one row per company:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Emails&lt;/strong&gt; from &lt;code&gt;mailto:&lt;/code&gt;, text, JSON-LD, Cloudflare-protected and "[at]/[dot]" forms. Each is tagged &lt;code&gt;sameDomain&lt;/code&gt;, &lt;code&gt;role&lt;/code&gt; (info@, sales@…) and &lt;code&gt;domainAcceptsMail&lt;/code&gt; (the MX check).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phones&lt;/strong&gt; from &lt;code&gt;tel:&lt;/code&gt; and schema.org. Numbers found only in text go into a separate &lt;code&gt;phonesUncertain&lt;/code&gt; list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;13 social networks&lt;/strong&gt; (LinkedIn, Facebook, Instagram, X, YouTube, TikTok, Pinterest, GitHub, Threads, Telegram, WhatsApp, Discord, Yelp), plus &lt;code&gt;contactPageUrls&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Company name and postal address when the site publishes them in schema.org or OpenGraph&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It crawls the homepage plus up to 19 contact/about/impressum/team pages on the same domain, honours robots.txt (including &lt;code&gt;Crawl-delay&lt;/code&gt;), goes 1 request per second per site and identifies itself as &lt;code&gt;ContactEnrichmentBot&lt;/code&gt;. It doesn't run JavaScript.&lt;/p&gt;

&lt;p&gt;The real result for the site above (trimmed):&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;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aloha-dental.com"&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;"companyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Aloha Dental"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"primaryEmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"riverside@aloha-dental.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phones"&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;"(512)707-7300"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phonesUncertain"&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;"(512) 494-5655"&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;"1614 E. Riverside DR., Austin, Texas, 78741, US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contactPageUrls"&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;"https://www.aloha-dental.com/contact-us/"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instagram"&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;"https://www.instagram.com/alohadentaltx"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pagesCrawled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"charged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;h2&gt;
  
  
  Real fill rates
&lt;/h2&gt;

&lt;p&gt;From live runs on 17 Oct 2026, measured over sites that were actually reached:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Websites&lt;/th&gt;
&lt;th&gt;Reached&lt;/th&gt;
&lt;th&gt;Email&lt;/th&gt;
&lt;th&gt;Phone&lt;/th&gt;
&lt;th&gt;Instagram&lt;/th&gt;
&lt;th&gt;LinkedIn&lt;/th&gt;
&lt;th&gt;Contact page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manchester restaurant/café sites&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;68%&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mixed: US trades/dentists, Paris cafés, 12 SaaS companies&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;55%&lt;/td&gt;
&lt;td&gt;57%&lt;/td&gt;
&lt;td&gt;69%&lt;/td&gt;
&lt;td&gt;59%&lt;/td&gt;
&lt;td&gt;89%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sites that weren't reached were blocked by robots.txt (7 and 4 sites, never crawled), returned HTTP 403/404, had no DNS, or timed out.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No JavaScript rendering.&lt;/strong&gt; Sites that only render contact details client-side return fewer results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some sites block data-centre IPs.&lt;/strong&gt; You can set an Apify Proxy, which Apify bills separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email isn't guaranteed.&lt;/strong&gt; Between a third and almost half of the reached sites in the tests had no email at all (32% and 45%). Many small businesses only offer a contact form, which is why &lt;code&gt;contactPageUrls&lt;/code&gt; is there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The MX check isn't verification.&lt;/strong&gt; It confirms the domain accepts mail, not that the mailbox exists. The tool never sends mail or probes mailboxes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;$0.002 per website where at least one email, phone or social profile was found&lt;/strong&gt; ($2 per 1,000). Websites that are unreachable, blocked by robots.txt or empty are free (&lt;code&gt;charged: false&lt;/code&gt;). In the mixed test, 47 of the 55 sites were charged, which is 9.4 cents. With a $0.01 max-cost cap, it charged exactly 5 sites and stopped. The script above is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play fair
&lt;/h2&gt;

&lt;p&gt;Named employees' emails and phones are personal data under GDPR, UK GDPR and CCPA, and you're the data controller. Have a lawful basis, honour opt-outs, follow CAN-SPAM, CASL and PECR, and if you want to minimise personal data, keep only the role addresses (&lt;code&gt;role: true&lt;/code&gt;). Please don't build spam lists with any of this.&lt;/p&gt;

&lt;p&gt;What obfuscation trick have you run into that I haven't listed? Tell me in the comments and I'll add a decoder for it.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>leadgeneration</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Domain Age, Email Provider and DMARC From a List of Domains: RDAP + DNS-over-HTTPS in Python</title>
      <dc:creator>Ben Barazandeh</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:08:21 +0000</pubDate>
      <link>https://dev.to/dedicated_crest/domain-age-email-provider-and-dmarc-from-a-list-of-domains-rdap-dns-over-https-in-python-5ge6</link>
      <guid>https://dev.to/dedicated_crest/domain-age-email-provider-and-dmarc-from-a-list-of-domains-rdap-dns-over-https-in-python-5ge6</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. Everything in the tutorial part works with free tools too.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A list of domains says a lot more than it looks. From public data alone you can tell how old a domain is, when it expires, who hosts its DNS, whether the company is on Google Workspace or Microsoft 365, and whether it has a DMARC policy. That's useful for fraud checks ("this domain is 12 days old"), deliverability audits, and segmenting a lead list ("Microsoft 365 shops with no DMARC").&lt;/p&gt;

&lt;p&gt;You don't need a WHOIS API subscription for most of it. You need two open protocols: &lt;strong&gt;RDAP&lt;/strong&gt; and &lt;strong&gt;DNS&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  RDAP in one paragraph
&lt;/h2&gt;

&lt;p&gt;RDAP is the official, structured JSON successor to WHOIS, run by the same registries. IANA publishes a bootstrap file (&lt;code&gt;data.iana.org/rdap/dns.json&lt;/code&gt;) that tells you which RDAP server handles each TLD. You look up the TLD, then call &lt;code&gt;{server}/domain/{name}&lt;/code&gt;. A 404 means the registry has no record, so the domain is probably available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Registration dates + DNS in ~30 lines
&lt;/h2&gt;

&lt;p&gt;This ran as-is on 25 Oct 2026. DNS goes through Cloudflare's public DNS-over-HTTPS JSON endpoint, so there's nothing to install beyond &lt;code&gt;requests&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rdap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tld&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;services&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;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;https://data.iana.org/rdap/dns.json&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;30&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;services&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tlds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tld&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tlds&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# this TLD has no RDAP server: fall back to port-43 WHOIS
&lt;/span&gt;    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/domain/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="si"&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;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;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eventAction&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eventDate&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="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;events&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;registered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ev&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;registration&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;expires&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ev&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;expiration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rtype&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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;https://cloudflare-dns.com/dns-query&lt;/span&gt;&lt;span class="sh"&gt;"&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;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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rtype&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                     &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accept&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;application/dns-json&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;30&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&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;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;Answer&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stripe.com&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;spiegel.de&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;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rdap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&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;  MX   &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;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;  DMARC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;dns&lt;/span&gt;&lt;span class="p"&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;_dmarc.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&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;TXT&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;Output (trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stripe.com {'registered': True, 'created': '1995-09-12T04:00:00Z', 'expires': '2027-09-11T04:00:00Z'}
  MX    ['10 aspmx.l.google.com.', '20 alt1.aspmx.l.google.com.']
  DMARC ['"v=DMARC1; p=reject; pct=100; fo=1; rua=mailto:dmarc-reports@stripe.com; ...']
spiegel.de None
  MX    ['0 spiegel-de.mail.protection.outlook.com.']
  DMARC ['"v=DMARC1; p=quarantine; rua=mailto:report.dmarc@spiegel.de; ...']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two facts fall straight out: Stripe's mail goes to Google (&lt;code&gt;aspmx.l.google.com&lt;/code&gt;), Der Spiegel's to Microsoft 365 (&lt;code&gt;mail.protection.outlook.com&lt;/code&gt;). And &lt;code&gt;.de&lt;/code&gt; has no RDAP server in IANA's bootstrap, so &lt;code&gt;rdap()&lt;/code&gt; returned &lt;code&gt;None&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ccTLDs are a patchwork.&lt;/strong&gt; Some have RDAP, some only port-43 WHOIS with their own text format, and some publish no dates at all. &lt;code&gt;.de&lt;/code&gt; doesn't publish registration dates. A few registries (for example &lt;code&gt;.es&lt;/code&gt;) offer no public RDAP or WHOIS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Registrable domain" isn't "last two labels".&lt;/strong&gt; &lt;code&gt;news.bbc.co.uk&lt;/code&gt; belongs to &lt;code&gt;bbc.co.uk&lt;/code&gt;, not &lt;code&gt;co.uk&lt;/code&gt;. Use the Public Suffix List, not string splitting. The same goes for turning emails and URLs into domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registrant data is mostly redacted&lt;/strong&gt; since GDPR. Expect the organisation and country at best.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS answers need cleaning.&lt;/strong&gt; MX records come with a priority prefix and trailing dot. TXT records come quoted and can be split into chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email provider is inference.&lt;/strong&gt; You map MX hosts (&lt;code&gt;google.com&lt;/code&gt;, &lt;code&gt;outlook.com&lt;/code&gt;, &lt;code&gt;zoho&lt;/code&gt;…) to providers, and SPF &lt;code&gt;include:&lt;/code&gt;s to sending tools (HubSpot, Salesforce, SendGrid…). It's reliable for the big providers and fuzzy for self-hosted setups.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DKIM can't be listed.&lt;/strong&gt; You can only probe common selectors, and some domains answer every selector (wildcards), which looks like a false positive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits.&lt;/strong&gt; Registry RDAP servers throttle bulk lookups. Add concurrency limits and backoff.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Or use the packaged version
&lt;/h2&gt;

&lt;p&gt;I packaged all of this as an Apify Actor, &lt;a href="https://apify.com/dedicated_crest/domain-whois-dns-intel" rel="noopener noreferrer"&gt;Bulk WHOIS &amp;amp; DNS Lookup&lt;/a&gt;. You paste domains, URLs or email addresses, and it normalises them to the registrable domain (&lt;code&gt;co.uk&lt;/code&gt;, &lt;code&gt;com.au&lt;/code&gt;…) and dedupes.&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;"domains"&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;"stripe.com"&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://www.apify.com/store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jane@hubspot.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bbc.co.uk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notion.so"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeWhois"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"includeDns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"includeWebsite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"concurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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;One row per domain: registrar and IANA ID, created/updated/expiry dates, &lt;strong&gt;domain age in days&lt;/strong&gt;, days until expiry, EPP status, nameservers, A/AAAA/MX/NS/TXT, SPF and its includes, DMARC record and policy, optional DKIM probing (with wildcard detection), &lt;strong&gt;email provider&lt;/strong&gt;, email security gateway, email senders, DNS provider, about 50 SaaS tools detected from DNS verification records, and an optional website check (status, redirects, title, basic tech like WordPress or Shopify). It's RDAP first with WHOIS fallback.&lt;/p&gt;

&lt;p&gt;A real run on 18 Oct 2026 with 7 inputs and every check on took about 46 seconds:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Registrar&lt;/th&gt;
&lt;th&gt;Created&lt;/th&gt;
&lt;th&gt;Email provider&lt;/th&gt;
&lt;th&gt;DMARC&lt;/th&gt;
&lt;th&gt;DNS provider&lt;/th&gt;
&lt;th&gt;SaaS found&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;stripe.com&lt;/td&gt;
&lt;td&gt;SafeNames Ltd.&lt;/td&gt;
&lt;td&gt;1995-09-12&lt;/td&gt;
&lt;td&gt;Google Workspace&lt;/td&gt;
&lt;td&gt;reject&lt;/td&gt;
&lt;td&gt;Amazon Route 53&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openai.com&lt;/td&gt;
&lt;td&gt;MarkMonitor Inc.&lt;/td&gt;
&lt;td&gt;2007-01-19&lt;/td&gt;
&lt;td&gt;Google Workspace&lt;/td&gt;
&lt;td&gt;reject&lt;/td&gt;
&lt;td&gt;Azure DNS&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hubspot.com&lt;/td&gt;
&lt;td&gt;MarkMonitor Inc.&lt;/td&gt;
&lt;td&gt;2005-02-06&lt;/td&gt;
&lt;td&gt;Google Workspace&lt;/td&gt;
&lt;td&gt;reject&lt;/td&gt;
&lt;td&gt;Cloudflare&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bbc.co.uk&lt;/td&gt;
&lt;td&gt;British Broadcasting Corpor…&lt;/td&gt;
&lt;td&gt;1994-12-13&lt;/td&gt;
&lt;td&gt;Other / self-hosted&lt;/td&gt;
&lt;td&gt;reject&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spiegel.de&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;Microsoft 365&lt;/td&gt;
&lt;td&gt;quarantine&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;ceo@hubspot.com&lt;/code&gt; and &lt;code&gt;https://www.apify.com&lt;/code&gt; in the input were normalised to their domains. Same &lt;code&gt;.de&lt;/code&gt; story as my script: DNS and email data, no WHOIS dates.&lt;/p&gt;

&lt;p&gt;Useful filters once it's in a sheet: &lt;code&gt;domainAgeDays &amp;lt; 90&lt;/code&gt; for new-domain risk, &lt;code&gt;daysUntilExpiry &amp;lt; 30&lt;/code&gt; for portfolio renewals, &lt;code&gt;emailProvider&lt;/code&gt; plus &lt;code&gt;dmarcPolicy&lt;/code&gt; for deliverability or security outreach.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WHOIS coverage depends on the registry.&lt;/strong&gt; No dates for &lt;code&gt;.de&lt;/code&gt;, nothing for TLDs with no public RDAP/WHOIS (you get a &lt;code&gt;whoisError&lt;/code&gt; note plus DNS data).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registrant fields are usually null&lt;/strong&gt; because of privacy redaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider and SaaS detection come from DNS records&lt;/strong&gt;, so they show what a domain has &lt;em&gt;verified&lt;/em&gt; or &lt;em&gt;routed&lt;/em&gt;, not what it actively uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The website check is slower.&lt;/strong&gt; Turn it off for big lists and raise concurrency (up to 20).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;registered: false&lt;/code&gt; is a strong hint, not a guarantee. Confirm at a registrar before you buy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;$0.003 per domain&lt;/strong&gt; ($3 per 1,000), with WHOIS/RDAP, DNS, email intelligence and the website check included. No start fee, and runs stop at your "maximum cost per run". The 7-domain test was about 2 cents. If you only need registrar and expiry, a WHOIS-only tool can be cheaper. And the script above is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play fair
&lt;/h2&gt;

&lt;p&gt;Some registries restrict reusing WHOIS data for marketing. Read their terms, and keep privacy laws in mind when you combine domain data with people.&lt;/p&gt;

&lt;p&gt;What signal would you want added: MTA-STS, BIMI, CAA, something else? Tell me in the comments.&lt;/p&gt;

</description>
      <category>python</category>
      <category>dns</category>
      <category>security</category>
      <category>api</category>
    </item>
    <item>
      <title>Find Companies That Just Raised Money: Parsing SEC Form D Filings from EDGAR</title>
      <dc:creator>Ben Barazandeh</dc:creator>
      <pubDate>Tue, 27 Oct 2026 14:03:30 +0000</pubDate>
      <link>https://dev.to/dedicated_crest/find-companies-that-just-raised-money-parsing-sec-form-d-filings-from-edgar-19bh</link>
      <guid>https://dev.to/dedicated_crest/find-companies-that-just-raised-money-parsing-sec-form-d-filings-from-edgar-19bh</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. Everything in the tutorial part works with free tools too.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"Recently funded companies" lists are a staple of B2B sales, recruiting and investing. A lot of them trace back to one public source: &lt;strong&gt;SEC Form D&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a US company raises money in a private placement under Regulation D, it has to file a Form D with the SEC within 15 days of the first sale. That makes it one of the earliest public signals of a raise, often before any press release. And the filing itself is structured XML with the company's address, &lt;strong&gt;business phone&lt;/strong&gt;, industry, amount offered, amount sold so far and the names of its executives and directors.&lt;/p&gt;

&lt;p&gt;Here's how to get it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule zero: SEC's fair-access policy
&lt;/h2&gt;

&lt;p&gt;EDGAR is free and needs no key, but the SEC asks every automated client to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;send a &lt;strong&gt;User-Agent that identifies you&lt;/strong&gt; with a contact email (unidentified traffic can be blocked), and&lt;/li&gt;
&lt;li&gt;stay under &lt;strong&gt;10 requests per second&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do both from the very first request. It's polite, and it's the difference between working code and a 403.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Find recent Form D filings and parse them
&lt;/h2&gt;

&lt;p&gt;EDGAR full-text search (the same backend as the search box on sec.gov) can filter by form type and date. Each hit points to the filing's &lt;code&gt;primary_doc.xml&lt;/code&gt;, which is the Form D itself. This script ran as-is on 25 Oct 2026:&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;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;etree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ElementTree&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;

&lt;span class="n"&gt;H&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;Ben Barazandeh ben.barazandeh@gmail.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;# SEC fair access: identify yourself
&lt;/span&gt;
&lt;span class="c1"&gt;# 1) Find recent Form D filings via EDGAR full-text search
&lt;/span&gt;&lt;span class="n"&gt;hits&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;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;https://efts.sec.gov/LATEST/search-index&lt;/span&gt;&lt;span class="sh"&gt;"&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;forms&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;D&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;dateRange&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;custom&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;startdt&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;2026-10-19&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;enddt&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;2026-10-23&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;H&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hits&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;hits&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;h&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&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="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cik&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_source&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;ciks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.sec.gov/Archives/edgar/data/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cik&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&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="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromstring&lt;/span&gt;&lt;span class="p"&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;get&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="n"&gt;H&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="n"&gt;content&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;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findtext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.//primaryIssuer/entityName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findtext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.//primaryIssuer/issuerPhoneNumber&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findtext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.//industryGroup/industryGroupType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findtext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.//offeringSalesAmounts/totalOfferingAmount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findtext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.//offeringSalesAmounts/totalAmountSold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# stay far below SEC's 10 requests/second
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Use your own name and email in the User-Agent.) Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Opus Databento CS LLC 9176692797 Other Technology 4000000 4000000
Five Winds Income Fund LLC 7758136887 Other 40000000 0
Advanced Glass Holdings, Inc. 847-906-5300 Other 2000000 2000000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The search reported 1,269 Form D hits for that Monday-to-Friday window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The pitfalls
&lt;/h2&gt;

&lt;p&gt;That output already shows several of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not every filer is a startup.&lt;/strong&gt; Many Form Ds come from investment funds and single-property real-estate LLCs. The usual first cut is to drop industry group "Pooled Investment Fund". It isn't perfect: "Five Winds Income Fund LLC" above reports its industry as "Other", so it would slip through that filter. Expect to eyeball names too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amounts aren't always numbers.&lt;/strong&gt; &lt;code&gt;totalOfferingAmount&lt;/code&gt; can be the word &lt;code&gt;Indefinite&lt;/code&gt;. Amount &lt;em&gt;sold&lt;/em&gt; can be 0 on a fresh filing, so "offering" isn't "raised".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amendments.&lt;/strong&gt; &lt;code&gt;D/A&lt;/code&gt; filings repeat earlier raises. Decide whether you want them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messy phones.&lt;/strong&gt; &lt;code&gt;9176692797&lt;/code&gt; vs &lt;code&gt;847-906-5300&lt;/code&gt;. Normalise before you load a CRM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filtering means parsing everything.&lt;/strong&gt; Full-text search can't filter by industry or amount, so to get "tech companies that raised $250K+" you have to download and parse every Form D in the window. That's roughly 300–500 per business day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Related persons are a list.&lt;/strong&gt; Executives, directors and promoters each have roles and sometimes a clarification. Flattening them sensibly takes some care.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Or use the packaged version
&lt;/h2&gt;

&lt;p&gt;I packaged this as an Apify Actor, &lt;a href="https://apify.com/dedicated_crest/sec-edgar-funding-leads" rel="noopener noreferrer"&gt;SEC EDGAR Scraper – Form D Funding Leads&lt;/a&gt;. It sends a descriptive User-Agent with a contact email, stays within SEC's rate limits and retries with backoff on throttling. Pooled investment funds are excluded by default.&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"formD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"filedWithinDays"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"industryGroups"&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;"Technology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Biotechnology"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Health Care"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minAmountSold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;250000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&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;Every lead is the whole Form D, flattened: entity type, state and year of incorporation, address, phone, industry group, revenue range, exemptions (506(b)/506(c)/504), date of first sale, securities offered, minimum investment, offering/sold/remaining amounts, investor counts, sales commissions and finders' fees, related persons with roles, and links to EDGAR.&lt;/p&gt;

&lt;p&gt;A real run on 18 Oct 2026 (filed in the last 7 days, max 25, funds excluded) saved 20 leads in 24 seconds. A few rows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Company&lt;/th&gt;
&lt;th&gt;Industry&lt;/th&gt;
&lt;th&gt;Offering&lt;/th&gt;
&lt;th&gt;Sold so far&lt;/th&gt;
&lt;th&gt;City&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Knowable Labs, Inc.&lt;/td&gt;
&lt;td&gt;Other Technology&lt;/td&gt;
&lt;td&gt;$310K&lt;/td&gt;
&lt;td&gt;$310K&lt;/td&gt;
&lt;td&gt;Saratoga, CA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lamina Surgical, Inc.&lt;/td&gt;
&lt;td&gt;Other Health Care&lt;/td&gt;
&lt;td&gt;$5M&lt;/td&gt;
&lt;td&gt;$490K&lt;/td&gt;
&lt;td&gt;Oakland, CA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StrikeArc Labs Inc.&lt;/td&gt;
&lt;td&gt;Other&lt;/td&gt;
&lt;td&gt;$500K&lt;/td&gt;
&lt;td&gt;$500K&lt;/td&gt;
&lt;td&gt;Birmingham, MI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eden X LLC&lt;/td&gt;
&lt;td&gt;Other Real Estate&lt;/td&gt;
&lt;td&gt;$25M&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;Lexington, KY&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There's also a second mode, &lt;strong&gt;filings search&lt;/strong&gt;: EDGAR full-text search across any form type (8-K, 10-K, S-1, Form 4, 13D…), with 8-K item codes translated to plain English. For example, 8-K "material cybersecurity incident" disclosures in the last 30 days:&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"filings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"forms"&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;"8-K"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query"&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="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;material cybersecurity incident&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&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;"filedWithinDays"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&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;For a daily feed, schedule it with &lt;code&gt;filedWithinDays: 1&lt;/code&gt; (or 2) and send rows to Sheets, a CRM or Slack.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Form D isn't every raise.&lt;/strong&gt; Companies that raise under other exemptions, or that file late or not at all, won't appear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fund filter is a first cut.&lt;/strong&gt; Pooled funds are excluded by industry group, but funds and real-estate LLCs that report another industry (like "Other") still get through. My sample above includes a $25M real-estate LLC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No emails.&lt;/strong&gt; Form D has a business phone and named people, not email addresses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Industry groups are coarse.&lt;/strong&gt; "Other Technology" covers a lot. Pair it with a keyword or a quick look at the website.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Industry or amount filters are slower.&lt;/strong&gt; Every Form D in the window has to be parsed, so expect roughly a minute per day of filings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;US filers only&lt;/strong&gt;, and not affiliated with the SEC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$0.005 per Form D lead&lt;/strong&gt; ($5 per 1,000), fully parsed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$0.002 per filing&lt;/strong&gt; in filings-search mode ($2 per 1,000)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No start fee. Filtered-out filings are free, and runs stop at your "maximum cost per run". The 20-lead test above was 10 cents in lead charges. Cheaper Form D Actors exist on the Store; I chose to parse the whole form and add the full-text mode. The DIY script above costs nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play fair
&lt;/h2&gt;

&lt;p&gt;The people named in a Form D are real people. Cold outreach is covered by CAN-SPAM, TCPA and, for EU/UK contacts, GDPR. Keep it relevant, and honour opt-outs.&lt;/p&gt;

&lt;p&gt;What would you add to a Form D lead: a website lookup, a funding-round guess, or something else? Tell me in the comments.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opendata</category>
      <category>api</category>
      <category>startup</category>
    </item>
    <item>
      <title>Track New Jobs at Your Target Companies Straight From Their ATS (Greenhouse, Lever, Ashby, Workday)</title>
      <dc:creator>Ben Barazandeh</dc:creator>
      <pubDate>Mon, 26 Oct 2026 20:27:05 +0000</pubDate>
      <link>https://dev.to/dedicated_crest/track-new-jobs-at-your-target-companies-straight-from-their-ats-greenhouse-lever-ashby-workday-5dk3</link>
      <guid>https://dev.to/dedicated_crest/track-new-jobs-at-your-target-companies-straight-from-their-ats-greenhouse-lever-ashby-workday-5dk3</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. Everything in the tutorial part works with free tools too.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you sell to companies that are hiring, recruit into them, or just want to know when a particular team opens a role, the company's own careers page is the best signal you can get. It's first-party and current, and you don't get job-board reposts or listings that expired three weeks ago.&lt;/p&gt;

&lt;p&gt;The nice surprise is that you rarely need to scrape HTML to read it. Most careers pages are a thin skin over an &lt;strong&gt;Applicant Tracking System (ATS)&lt;/strong&gt;, and most ATSs publish a public JSON feed for exactly this purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Find the ATS behind a careers page
&lt;/h2&gt;

&lt;p&gt;Open the careers page, click any job, and look at the URL:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL looks like&lt;/th&gt;
&lt;th&gt;ATS&lt;/th&gt;
&lt;th&gt;Public feed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;boards.greenhouse.io/stripe&lt;/code&gt; or &lt;code&gt;job-boards.greenhouse.io/...&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Greenhouse&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boards-api.greenhouse.io/v1/boards/{slug}/jobs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobs.lever.co/spotify&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lever&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api.lever.co/v0/postings/{slug}?mode=json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jobs.ashbyhq.com/openai&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ashby&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api.ashbyhq.com/posting-api/job-board/{slug}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nvidia.wd5.myworkdayjobs.com/...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Workday&lt;/td&gt;
&lt;td&gt;POST search endpoint per tenant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The slug in the URL is what you pass to the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Call the feeds yourself
&lt;/h2&gt;

&lt;p&gt;These three calls worked when I ran them on 25 Oct 2026:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Greenhouse: every open job at Stripe&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://boards-api.greenhouse.io/v1/boards/stripe/jobs"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json,sys; j=json.load(sys.stdin)['jobs']; print(len(j), j[0]['title'], '|', j[0]['location']['name'])"&lt;/span&gt;

&lt;span class="c"&gt;# Lever: Spotify&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.lever.co/v0/postings/spotify?mode=json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json,sys; j=json.load(sys.stdin); print(len(j), j[0]['text'])"&lt;/span&gt;

&lt;span class="c"&gt;# Ashby: OpenAI, with structured compensation&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.ashbyhq.com/posting-api/job-board/openai?includeCompensation=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json,sys; j=json.load(sys.stdin)['jobs']; print(len(j), j[0]['title'])"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was 719 jobs at Stripe, 90 at Spotify and 793 at OpenAI. No key, no login, no browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The pitfalls
&lt;/h2&gt;

&lt;p&gt;Once you go past three companies, this is where the time goes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every ATS has its own schema.&lt;/strong&gt; Greenhouse gives you &lt;code&gt;location.name&lt;/code&gt;, Lever &lt;code&gt;categories.location&lt;/code&gt;, Ashby &lt;code&gt;location&lt;/code&gt; plus &lt;code&gt;secondaryLocations&lt;/code&gt;. Department, remote flag and employment type all have different names and shapes. You'll write a normaliser per ATS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salary is only sometimes structured.&lt;/strong&gt; Ashby, Lever and Recruitee can return min/max/currency. Elsewhere, if the company publishes pay at all, it's buried in the description HTML and you have to parse ranges like "$250K - $445K" yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lever has an EU host.&lt;/strong&gt; Some companies live on &lt;code&gt;jobs.eu.lever.co&lt;/code&gt; and need the EU API host (&lt;code&gt;api.eu.lever.co&lt;/code&gt;) instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workday is the awkward one.&lt;/strong&gt; Each tenant has its own host and site name, you POST a search, and it caps at about 2,000 results per search. For a big employer you need keyword searches to reach specific roles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"New jobs" needs a date.&lt;/strong&gt; To get a "posted today" feed, you need each ATS's published/updated date, which also differs per platform. Dedupe on the ATS job ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be gentle.&lt;/strong&gt; These are public feeds, but one request per board per day is plenty for monitoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it is hard. It's just a lot of small adapters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Or use the packaged version
&lt;/h2&gt;

&lt;p&gt;I wrapped seven ATSs (Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters and Recruitee) into one Apify Actor, &lt;a href="https://apify.com/dedicated_crest/ats-career-jobs-scraper" rel="noopener noreferrer"&gt;Career Site Jobs API&lt;/a&gt;. You can pass career URLs, &lt;code&gt;ats:slug&lt;/code&gt;, or just a bare company name, and it tries to detect the ATS:&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;"companies"&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;"https://jobs.ashbyhq.com/openai"&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://boards.greenhouse.io/airbnb"&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://jobs.lever.co/spotify"&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://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stripe"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&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;"engineer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItemsPerCompany"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&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;Every row has the same fields whatever the ATS: &lt;code&gt;company, ats, jobId, title, department, location, remote, employmentType, salaryMin, salaryMax, salaryCurrency, postedAt, url, applyUrl, description&lt;/code&gt; and a few more. Fields an ATS doesn't publish come back as &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's what that input returned in a real run on 17 Oct 2026 (15 jobs, about 7 seconds). The log reported 814 open jobs at OpenAI, 173 at Airbnb, 82 at Spotify, 732 at Stripe (auto-detected from the plain name &lt;code&gt;stripe&lt;/code&gt;), and 2,000 at NVIDIA, which is Workday's search cap, not the real total.&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&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Salary&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;openai&lt;/td&gt;
&lt;td&gt;Research Engineer&lt;/td&gt;
&lt;td&gt;San Francisco&lt;/td&gt;
&lt;td&gt;$250K - $445K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;greenhouse&lt;/td&gt;
&lt;td&gt;Airbnb&lt;/td&gt;
&lt;td&gt;Engineering Manager, Guest &amp;amp; Host&lt;/td&gt;
&lt;td&gt;London, United Kingdom&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lever&lt;/td&gt;
&lt;td&gt;spotify&lt;/td&gt;
&lt;td&gt;AV Engineer - Platform&lt;/td&gt;
&lt;td&gt;Stockholm&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;workday&lt;/td&gt;
&lt;td&gt;nvidia&lt;/td&gt;
&lt;td&gt;Director, Engineering – Software Engineering&lt;/td&gt;
&lt;td&gt;Vietnam, Hanoi&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;greenhouse&lt;/td&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;td&gt;AI Engineer&lt;/td&gt;
&lt;td&gt;Chicago&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(The counts differ from my 25 Oct curl calls above because boards change daily. That's the point of reading them live.)&lt;/p&gt;

&lt;p&gt;Filters: title keywords to include or exclude, location with aliases (&lt;code&gt;United States&lt;/code&gt; also matches entries like &lt;code&gt;US, CA, Santa Clara&lt;/code&gt;), remote only, department, and &lt;code&gt;postedWithinDays&lt;/code&gt;. Each run also writes a per-company SUMMARY record with the detected ATS, jobs on the board, jobs saved and any errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup I'd actually use
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Put your 20–200 target accounts in &lt;code&gt;companies&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;postedWithinDays: 1&lt;/code&gt; (or 2, to be safe with time zones).&lt;/li&gt;
&lt;li&gt;Schedule it daily in Apify and connect Google Sheets, Slack or a webhook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You get a morning list of the roles each target company opened yesterday. If you set &lt;code&gt;includeDescription: false&lt;/code&gt;, runs are lighter and faster.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It isn't a job board.&lt;/strong&gt; It reads the companies you give it. It can't search "all companies hiring Rust engineers".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seven ATSs only.&lt;/strong&gt; iCIMS, Taleo, SuccessFactors, custom in-house career sites and LinkedIn/Indeed aren't covered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workday tops out around 2,000 jobs per search.&lt;/strong&gt; Add keywords for very large employers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salary only where the company publishes it.&lt;/strong&gt; In the sample above, only the Ashby rows had pay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-detection by name can miss.&lt;/strong&gt; If &lt;code&gt;acme&lt;/code&gt; isn't found, paste the careers URL instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;$0.002 per job&lt;/strong&gt; ($2 per 1,000), with no start fee. You pay only for jobs saved after your filters, and the run stops cleanly at your "maximum cost per run". The 15-job test above was 3 cents in job charges. The DIY route costs nothing but your time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play fair
&lt;/h2&gt;

&lt;p&gt;Use the data in line with each company's terms, and don't use hiring signals to spam recruiters or hiring managers. A short, relevant note beats a sequence of ten.&lt;/p&gt;

&lt;p&gt;Which ATS or Workday tenant breaks for you? I'd love to collect edge cases in the comments, especially tenants with odd site names.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>api</category>
      <category>recruiting</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Local Business Leads Without Scraping Google Maps: An OpenStreetMap + Overpass Guide</title>
      <dc:creator>Ben Barazandeh</dc:creator>
      <pubDate>Sun, 25 Oct 2026 19:34:25 +0000</pubDate>
      <link>https://dev.to/dedicated_crest/local-business-leads-without-scraping-google-maps-an-openstreetmap-overpass-guide-1igf</link>
      <guid>https://dev.to/dedicated_crest/local-business-leads-without-scraping-google-maps-an-openstreetmap-overpass-guide-1igf</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I'm Ben, and I built the Apify Actor mentioned later in this post. I charge per result for it. Everything in the tutorial part works with free tools too.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you sell to local businesses (web design, SEO, payments, insurance, supplies), you need lists like "every roofer in Denver with a phone number". Most people reach for a Google Maps scraper. That works, but Google's terms forbid scraping, and you aren't allowed to store or resell what you collect. That's a shaky foundation for a CRM you plan to keep for years.&lt;/p&gt;

&lt;p&gt;There's an open alternative a lot of people overlook: &lt;strong&gt;OpenStreetMap&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenStreetMap?
&lt;/h2&gt;

&lt;p&gt;OpenStreetMap (OSM) is the community-built world map behind thousands of apps. Its data is licensed under the &lt;strong&gt;Open Database License (ODbL)&lt;/strong&gt;. You can store it, reuse it and even resell it, as long as you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;credit "© OpenStreetMap contributors" when you use it publicly, and&lt;/li&gt;
&lt;li&gt;keep any database you &lt;em&gt;publish&lt;/em&gt; from it under the ODbL too (share-alike).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;OSM is full of businesses: shops, restaurants, clinics, offices and tradespeople, tagged with things like &lt;code&gt;amenity=dentist&lt;/code&gt;, &lt;code&gt;craft=plumber&lt;/code&gt; or &lt;code&gt;shop=bakery&lt;/code&gt;. Many have &lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;website&lt;/code&gt;, &lt;code&gt;opening_hours&lt;/code&gt; and sometimes &lt;code&gt;email&lt;/code&gt; (or &lt;code&gt;contact:*&lt;/code&gt; variants).&lt;/p&gt;

&lt;p&gt;Here are the trade-offs up front:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;OpenStreetMap&lt;/th&gt;
&lt;th&gt;Google Maps scrapers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Licence&lt;/td&gt;
&lt;td&gt;ODbL: store and reuse with attribution&lt;/td&gt;
&lt;td&gt;Google ToS forbid scraping and storing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ratings and reviews&lt;/td&gt;
&lt;td&gt;Not in OSM&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage&lt;/td&gt;
&lt;td&gt;Very good in Europe and big cities, uneven for trades in small towns&lt;/td&gt;
&lt;td&gt;Most complete&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you need star ratings, OSM can't help. If you need a contactable list you can actually keep, it's worth a look.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Query OSM yourself with Overpass
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Overpass API&lt;/strong&gt; is a read-only query engine over OSM data. The basic pattern for a lead list is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;find the city's &lt;strong&gt;administrative boundary&lt;/strong&gt; and turn it into an area&lt;/li&gt;
&lt;li&gt;ask for every node, way and relation (&lt;code&gt;nwr&lt;/code&gt;) with your tag inside that area&lt;/li&gt;
&lt;li&gt;get back tags plus a centre point&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A minimal Overpass QL query looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[out:json][timeout:60];
area["boundary"="administrative"]["name"="Denver"]-&amp;gt;.city;
nwr["craft"="plumber"](area.city);
out center tags;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste it into &lt;a href="https://overpass-turbo.eu" rel="noopener noreferrer"&gt;overpass-turbo.eu&lt;/a&gt; to see results on a map. In a real pipeline you'll quickly hit the annoying parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ambiguous names.&lt;/strong&gt; Lots of places are called "Denver" or "Manchester", so you need to pin down the right boundary (country, state, &lt;code&gt;admin_level&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Busy public servers.&lt;/strong&gt; Public Overpass instances often reply "try again later". You need retries with backoff, and ideally failover between servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage policies.&lt;/strong&gt; Public instances are run by volunteers and companies, each with its own policy. overpass-api.de, for example, asks commercial users to self-host or pay. Read the policy before you point a business workload at a server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messy tags.&lt;/strong&gt; &lt;code&gt;phone&lt;/code&gt; vs &lt;code&gt;contact:phone&lt;/code&gt;, several values separated by &lt;code&gt;;&lt;/code&gt;, websites without &lt;code&gt;https://&lt;/code&gt;, and addresses split across &lt;code&gt;addr:*&lt;/code&gt; tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deduplication&lt;/strong&gt; when you combine several categories or cities.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is hard, but it adds up to a weekend of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Or use the packaged version
&lt;/h2&gt;

&lt;p&gt;I packaged all of that into an Apify Actor, &lt;a href="https://apify.com/dedicated_crest/osm-local-business-leads" rel="noopener noreferrer"&gt;Local Business Leads&lt;/a&gt;. The input is plain English:&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;"categories"&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;"plumber"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"electrician"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hvac"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"roofer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"locations"&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;"Denver, CO"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enrichWebsites"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&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="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;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;95 built-in categories&lt;/strong&gt; (dentist, hvac, hair salon, lawyer, accountant, gym, veterinarian, hotel…), plus any raw OSM tag such as &lt;code&gt;shop=bakery&lt;/code&gt;, &lt;code&gt;craft=*&lt;/code&gt; or &lt;code&gt;cuisine~pizza&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Resolves "Denver, CO" to Denver's &lt;strong&gt;official city boundary&lt;/strong&gt; in OSM, so it doesn't spill into the next town. It can also search a radius around a lat/lng.&lt;/li&gt;
&lt;li&gt;Resolves locations with Overpass itself, without calling the public Nominatim geocoder&lt;/li&gt;
&lt;li&gt;Uses only public Overpass instances whose published policy allows use in any project: &lt;strong&gt;VK Maps&lt;/strong&gt; and &lt;strong&gt;Private.coffee&lt;/strong&gt;. It doesn't use overpass-api.de. Queries run one at a time, with exponential backoff and failover. For big or frequent jobs you can plug in your own Overpass endpoint.&lt;/li&gt;
&lt;li&gt;Puts contact-rich leads first, and can filter to businesses that have a phone, website, email or any contact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's one real lead from a Denver roofers run (trimmed):&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;"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;"Assurance Contractors"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"roofer"&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;"1660 South Albion Street Suite 339, Denver, CO 80222"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"+1-720-837-1630"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"service@assurancecontractors.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"website"&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://assurancecontractors.com/roofing-denver-co/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"openingHours"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mo-Fr 09:00-17:00; Sa-Su closed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"latitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;39.6863292&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"longitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-104.9391307&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"osmId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node/9053239725"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"osmUrl"&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://www.openstreetmap.org/node/9053239725"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastEditedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-09-20T05:33:48Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"websiteEmails"&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;"info@assurancecontractors.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"websitePagesFetched"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attribution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"© OpenStreetMap contributors, ODbL 1.0 – https://www.openstreetmap.org/copyright"&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;Each row also has address parts, socials (Facebook, Instagram, LinkedIn, X, YouTube, TikTok), brand, operator, cuisine, wheelchair access and a Wikidata ID where they're mapped. Every row carries an &lt;code&gt;attribution&lt;/code&gt; field, an OSM link and a last-edited date, so you can see how fresh the entry is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Be honest about coverage
&lt;/h2&gt;

&lt;p&gt;This is the part most lead tools skip. Here are real fill rates from my live test runs on 17 Oct 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Search&lt;/th&gt;
&lt;th&gt;Leads&lt;/th&gt;
&lt;th&gt;Phone&lt;/th&gt;
&lt;th&gt;Website&lt;/th&gt;
&lt;th&gt;Email&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Restaurants + cafés, Manchester UK (only with phone or website)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;55%&lt;/td&gt;
&lt;td&gt;87%&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plumbers/electricians/HVAC/roofers, Denver CO + enrichment&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;43%&lt;/td&gt;
&lt;td&gt;25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dentists, Austin TX + enrichment (capped at $0.05)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;shop=bakery&lt;/code&gt; within 2 km of central Paris&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pattern is clear. Food, shops, healthcare and hotels in big cities are well mapped. &lt;strong&gt;Trades in US cities are thin.&lt;/strong&gt; 32 trade businesses across four categories in Denver is far fewer than Google would show. There are also no ratings or reviews. Use OSM where it's strong, and don't expect it to replace Google for every niche.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Fill in emails from each business's own website
&lt;/h2&gt;

&lt;p&gt;Email is the field OSM most often lacks. The fix is to visit each business's own website and read the contact details there.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;enrichWebsites: true&lt;/code&gt;, the Actor reads the homepage plus up to 4 contact/about/impressum pages. It stays on the business's own domain, respects robots.txt, sends at most 1 request per second per site and uses an identifiable User-Agent. In the Denver run, enrichment roughly doubled email coverage, from 12% (OSM tags alone) to 25%.&lt;/p&gt;

&lt;p&gt;If you're building this yourself, the same rules apply: be polite, stay on the business's own domain, and honour robots.txt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export and automate
&lt;/h2&gt;

&lt;p&gt;Results land in an Apify dataset that you can download as CSV, Excel or JSON, or push to Google Sheets, HubSpot, Make, Zapier or n8n. To keep a list up to date, schedule a monthly run per city and dedupe on &lt;code&gt;osmId&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$0.003 per lead&lt;/strong&gt; ($3 per 1,000)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$0.002 per website enriched&lt;/strong&gt; ($2 per 1,000), charged only when enrichment is on and the site was reached&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You pay only for leads returned, and runs stop cleanly at your "maximum cost per run". The Austin dentist test stopped at 10 leads + 9 enrichments = $0.048. The free route (Overpass plus your own crawler) costs nothing but time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Play fair
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Credit "© OpenStreetMap contributors" when you use the data publicly, and remember the share-alike rule if you publish a derived database.&lt;/li&gt;
&lt;li&gt;Contact data comes with obligations. GDPR, CAN-SPAM, CASL and PECR rules apply to cold email. Honour opt-outs.&lt;/li&gt;
&lt;li&gt;Go easy on the public Overpass servers. Self-host or pay for an endpoint if you run big jobs.&lt;/li&gt;
&lt;li&gt;If a business's details in OSM are wrong, fix them on &lt;a href="https://www.openstreetmap.org" rel="noopener noreferrer"&gt;openstreetmap.org&lt;/a&gt;. The map gets better for everyone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Did a city come back thin, or do you have thoughts on the Overpass usage? Tell me in the comments. I'm especially keen to hear from OSM folks.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Map data © &lt;a href="https://www.openstreetmap.org/copyright" rel="noopener noreferrer"&gt;OpenStreetMap contributors&lt;/a&gt;, &lt;a href="https://opendatacommons.org/licenses/odbl/" rel="noopener noreferrer"&gt;ODbL 1.0&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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