<?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: SwiftKit</title>
    <description>The latest articles on DEV Community by SwiftKit (@swiftkit_dev).</description>
    <link>https://dev.to/swiftkit_dev</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%2F4156157%2Fffc5cdd4-5e31-4a24-8de3-56f09ee000f7.png</url>
      <title>DEV Community: SwiftKit</title>
      <link>https://dev.to/swiftkit_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swiftkit_dev"/>
    <language>en</language>
    <item>
      <title>Validate email lists without sending a single email (DNS, typos, disposables, duplicates)</title>
      <dc:creator>SwiftKit</dc:creator>
      <pubDate>Fri, 02 Oct 2026 05:26:13 +0000</pubDate>
      <link>https://dev.to/swiftkit_dev/validate-email-lists-without-sending-a-single-email-dns-typos-disposables-duplicates-2m9l</link>
      <guid>https://dev.to/swiftkit_dev/validate-email-lists-without-sending-a-single-email-dns-typos-disposables-duplicates-2m9l</guid>
      <description>&lt;p&gt;Most "email verification" tutorials end with the same trick: open an SMTP connection to the recipient's mail server and ask whether the mailbox exists. It works less and less. Gmail, Outlook and most corporate servers accept everything (catch-all) or answer vaguely, and probing from a cloud IP is a quick way to get that IP blocklisted.&lt;/p&gt;

&lt;p&gt;The good news: you can catch most of the junk in a list &lt;strong&gt;without contacting any mail server at all.&lt;/strong&gt; Here's the checklist I ended up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Syntax, but realistic
&lt;/h2&gt;

&lt;p&gt;The full RFC 5322 grammar allows things no real provider accepts (quoted local parts, comments, IP literals). For list cleaning, a practical rule is better: one &lt;code&gt;@&lt;/code&gt;, a local part of normal characters, and a domain with a real TLD. Lowercase the domain, trim whitespace, and strip &lt;code&gt;mailto:&lt;/code&gt; that people paste in.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Does the domain accept mail?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Resolver&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:dns/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Resolver&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setServers&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.1.1.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8.8.8.8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;acceptsMail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveMx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// A "null MX" (RFC 7505) means: this domain never receives email.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;mx&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="nx"&gt;exchange&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;mx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ENODATA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// No MX: mail falls back to the A record.&lt;/span&gt;
      &lt;span class="k"&gt;try&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ENOTFOUND&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// domain doesn't exist&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// timeout etc.: report "unknown", don't guess&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Treat timeouts as &lt;strong&gt;unknown&lt;/strong&gt;, not invalid. Marking a real customer invalid because a DNS server hiccuped is worse than leaving one bad address in.&lt;/li&gt;
&lt;li&gt;Cache per domain. A 50k list usually has only a few thousand distinct domains.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Typos of big providers
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gmial.com&lt;/code&gt;, &lt;code&gt;hotmal.com&lt;/code&gt;, &lt;code&gt;gmail.con&lt;/code&gt;. These often &lt;em&gt;do&lt;/em&gt; resolve (someone registered them), so DNS alone won't catch them. Compare the domain with a short list of popular providers using edit distance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;editDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;j&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="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;d&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="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;j&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="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="nx"&gt;j&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="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&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="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="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="nx"&gt;j&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="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&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="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&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="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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// distance 1–2 from gmail.com, outlook.com, yahoo.com… → suggest the fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only suggest when the domain is &lt;em&gt;not&lt;/em&gt; itself a known provider, or you'll "correct" &lt;code&gt;gmx.com&lt;/code&gt; to &lt;code&gt;gmail.com&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Disposable and role addresses
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Disposable:&lt;/strong&gt; the community-maintained &lt;a href="https://github.com/disposable-email-domains/disposable-email-domains" rel="noopener noreferrer"&gt;disposable-email-domains&lt;/a&gt; list (public domain, about 9,000 entries) catches most throwaway services. Refresh it regularly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role accounts:&lt;/strong&gt; &lt;code&gt;info@&lt;/code&gt;, &lt;code&gt;sales@&lt;/code&gt;, &lt;code&gt;support@&lt;/code&gt;, &lt;code&gt;noreply@&lt;/code&gt;. They're real, but they're shared inboxes, and many email tools treat them as a risk. Flag them instead of deleting them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Duplicates that don't look like duplicates
&lt;/h2&gt;

&lt;p&gt;For Gmail, dots and &lt;code&gt;+tags&lt;/code&gt; don't matter, so these are all the same inbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight email"&gt;&lt;code&gt;&lt;span class="nt"&gt;john.smith+news@gmail.com
johnsmith@googlemail.com
JohnSmith@gmail.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build a &lt;strong&gt;canonical form&lt;/strong&gt; (lowercase, remove dots and the &lt;code&gt;+tag&lt;/code&gt; for Gmail, map googlemail.com to gmail.com) and dedupe on that. Only remove &lt;code&gt;+tags&lt;/code&gt; for providers where you know they're aliases.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Cheap extra signals
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Domain age&lt;/strong&gt; from the registry's RDAP service: a domain registered last week on a B2B sign-up form deserves a second look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parked domains:&lt;/strong&gt; name servers like &lt;code&gt;sedoparking.com&lt;/code&gt; mean "this domain is for sale", not a real company.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SPF / DMARC&lt;/strong&gt; presence: not proof of anything alone, but real sending domains usually have them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this can't tell you
&lt;/h2&gt;

&lt;p&gt;Honestly: none of this proves that a &lt;em&gt;specific mailbox&lt;/em&gt; exists. "Valid" here means "well formed, the domain is real and accepts mail, and nothing looks off." For list hygiene before an import, that's usually the part that matters, and it's fast, cheap and doesn't touch your sender reputation.&lt;/p&gt;

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

&lt;p&gt;I packaged this checklist as an Apify Actor: &lt;strong&gt;&lt;a href="https://apify.com/swiftkit/email-check" rel="noopener noreferrer"&gt;Bulk Email Validator&lt;/a&gt;&lt;/strong&gt;. Paste emails or a CSV link, and you get &lt;code&gt;valid / risky / invalid&lt;/code&gt; with the reason, a typo suggestion, disposable/role/free flags, mail provider, domain age, parked and SPF/DMARC flags, and duplicate detection. It never sends a test email. Pay-per-email, about $0.50 per 1,000.&lt;/p&gt;

&lt;p&gt;What's the weirdest thing you've found in an email list?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with AI assistance; the snippets and numbers were tested before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>email</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to detect any website's tech stack in bulk (and why a real browser finds 2x more)</title>
      <dc:creator>SwiftKit</dc:creator>
      <pubDate>Fri, 02 Oct 2026 03:14:59 +0000</pubDate>
      <link>https://dev.to/swiftkit_dev/how-to-detect-any-websites-tech-stack-in-bulk-and-why-a-real-browser-finds-2x-more-4dfb</link>
      <guid>https://dev.to/swiftkit_dev/how-to-detect-any-websites-tech-stack-in-bulk-and-why-a-real-browser-finds-2x-more-4dfb</guid>
      <description>&lt;p&gt;"What is this site built with?" sounds like a one-off question, until you need the answer for 5,000 domains. Sales teams want every Shopify store in a list. Agencies want to audit prospects. Analysts want to count which CMS wins in a niche.&lt;/p&gt;

&lt;p&gt;Here's how technology detection actually works, and what I learned building an open-source detector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fingerprints: the core idea
&lt;/h2&gt;

&lt;p&gt;Every tool leaves traces. The open-source &lt;a href="https://github.com/enthec/webappanalyzer" rel="noopener noreferrer"&gt;webappanalyzer&lt;/a&gt; project (a community continuation of the old Wappalyzer data) catalogs &lt;strong&gt;7,600+ technologies&lt;/strong&gt; with patterns like:&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="nl"&gt;"WordPress"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"generator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^WordPress ?([&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;d.]+)?&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;;version:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;link rel=[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;']stylesheet[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;'] [^&amp;gt;]+/wp-(?:content|includes)/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"implies"&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;"PHP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MySQL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A detector loads the page and tests each pattern against:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTML&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/wp-content/&lt;/code&gt; paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Script URLs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cdn.shopify.com/s/…&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meta tags&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;meta name="generator" content="WordPress 6.8"&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response headers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;server: cloudflare&lt;/code&gt;, &lt;code&gt;x-powered-by: Next.js&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cookies&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_shopify_y&lt;/code&gt;, &lt;code&gt;hubspotutk&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;MX records → Google Workspace or Microsoft 365, SPF includes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSL issuer&lt;/td&gt;
&lt;td&gt;Let's Encrypt, DigiCert…&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then it resolves &lt;strong&gt;implies&lt;/strong&gt; (WordPress → PHP + MySQL), &lt;strong&gt;requires&lt;/strong&gt; and &lt;strong&gt;excludes&lt;/strong&gt;, and keeps only matches above a confidence threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: half the fingerprints need JavaScript
&lt;/h2&gt;

&lt;p&gt;When I counted, &lt;strong&gt;3,342 of 7,628 technologies&lt;/strong&gt; have &lt;code&gt;js&lt;/code&gt; patterns: things like &lt;code&gt;window.ABTasty&lt;/code&gt;, &lt;code&gt;window.Intercom&lt;/code&gt; or &lt;code&gt;AFRAME.version&lt;/code&gt;. These only exist after the page's JavaScript runs. A plain HTTP request never sees them, and neither do tools that load later through tag managers.&lt;/p&gt;

&lt;p&gt;So I ran the same sites both ways:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;HTML + headers + DNS&lt;/th&gt;
&lt;th&gt;Headless Chrome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;notion.so&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;38&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;allbirds.com&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;29&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hubspot.com&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly &lt;strong&gt;2× more detections&lt;/strong&gt; with a real browser: analytics, chat widgets, A/B testing, consent tools and front-end frameworks.&lt;/p&gt;

&lt;p&gt;Tips if you build this yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Block images, fonts and media&lt;/strong&gt; in the browser. You don't need them, and it makes each page much cheaper.&lt;/li&gt;
&lt;li&gt;Evaluate all JS chains in &lt;strong&gt;one&lt;/strong&gt; &lt;code&gt;page.evaluate&lt;/code&gt; call, wrapped in try/catch (some getters throw).&lt;/li&gt;
&lt;li&gt;Also feed the browser's &lt;strong&gt;network requests&lt;/strong&gt; into the script-URL and XHR patterns. Many tools load from tag managers you won't see in the initial HTML.&lt;/li&gt;
&lt;li&gt;Respect &lt;code&gt;robots.txt&lt;/code&gt;, and expect some big sites to answer 403 to automation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Open source + a hosted version
&lt;/h2&gt;

&lt;p&gt;The detector is open source (GPL-3.0, like the fingerprint data):&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/swiftkit-dev/website-tech-stack-detector" rel="noopener noreferrer"&gt;github.com/swiftkit-dev/website-tech-stack-detector&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you'd rather not run browsers yourself, the same code runs on Apify as&lt;br&gt;
&lt;strong&gt;&lt;a href="https://apify.com/swiftkit/tech-stack" rel="noopener noreferrer"&gt;Website Tech Stack Detector&lt;/a&gt;&lt;/strong&gt;: paste a list of domains,&lt;br&gt;
choose fast or deep scan, and get JSON/CSV with technologies grouped by category, versions and the&lt;br&gt;
email provider. It's pay-per-site, and unreachable sites are free.&lt;/p&gt;

&lt;p&gt;What would you use bulk tech detection for? I'm curious which categories people care about most.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with AI assistance; the endpoints, numbers and examples were tested before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>marketing</category>
    </item>
    <item>
      <title>How to get every open job from Greenhouse, Lever, Ashby and Workday with one API call</title>
      <dc:creator>SwiftKit</dc:creator>
      <pubDate>Fri, 02 Oct 2026 03:14:42 +0000</pubDate>
      <link>https://dev.to/swiftkit_dev/how-to-get-every-open-job-from-greenhouse-lever-ashby-and-workday-with-one-api-call-151k</link>
      <guid>https://dev.to/swiftkit_dev/how-to-get-every-open-job-from-greenhouse-lever-ashby-and-workday-with-one-api-call-151k</guid>
      <description>&lt;p&gt;If you've ever built a niche job board, a hiring-signal feed for sales, or just wanted to track who's hiring data engineers, you've probably hit the same wall: every company's careers page looks different.&lt;/p&gt;

&lt;p&gt;The good news is that under the hood, most of them run on a small number of &lt;strong&gt;applicant tracking systems (ATS)&lt;/strong&gt;, and many of those publish open jobs through &lt;strong&gt;public, unauthenticated endpoints&lt;/strong&gt;. They exist precisely so companies can show their openings anywhere. You don't need to scrape HTML.&lt;/p&gt;

&lt;p&gt;Here's how the big ones work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Greenhouse
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://boards-api.greenhouse.io/v1/boards/airbnb/jobs?content=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;airbnb&lt;/code&gt; part is the company's &lt;em&gt;board token&lt;/em&gt;, the same slug you see in &lt;code&gt;boards.greenhouse.io/airbnb&lt;/code&gt;. You get title, location, departments, first-published date and (with &lt;code&gt;content=true&lt;/code&gt;) the full description as HTML-escaped text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lever
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://api.lever.co/v0/postings/palantir?mode=json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lever gives you &lt;code&gt;categories.commitment&lt;/code&gt; (full-time, contract…), &lt;code&gt;workplaceType&lt;/code&gt; (remote/hybrid/onsite), and a &lt;code&gt;salaryRange&lt;/code&gt; object when the company publishes pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ashby
&lt;/h2&gt;



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

&lt;/div&gt;



&lt;p&gt;Ashby is the friendliest for salary data: &lt;code&gt;includeCompensation=true&lt;/code&gt; returns structured min/max, currency and interval. In a quick test, OpenAI's "Data Engineer" role came back as &lt;strong&gt;$235K – $385K&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workday
&lt;/h2&gt;

&lt;p&gt;Workday is the one everyone asks about, and the trickiest. Each company's career site exposes a JSON endpoint behind its public page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://nvidia.wd5.myworkdayjobs.com/wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"appliedFacets":{},"limit":20,"offset":0,"searchText":""}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You &lt;strong&gt;can't guess&lt;/strong&gt; the URL from a company name. Tenant (&lt;code&gt;nvidia&lt;/code&gt;), data center (&lt;code&gt;wd5&lt;/code&gt;) and site name (&lt;code&gt;NVIDIAExternalCareerSite&lt;/code&gt;) all vary, so start from the company's careers link.&lt;/li&gt;
&lt;li&gt;Pages are capped at &lt;strong&gt;20 jobs&lt;/strong&gt;, and the &lt;code&gt;total&lt;/code&gt; field is only filled in on the first page. If you read it on page 2, you'll stop early. (Ask me how I know.)&lt;/li&gt;
&lt;li&gt;Dates come as text like "Posted 3 Days Ago", so you only get day precision.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The annoying part: normalizing everything
&lt;/h2&gt;

&lt;p&gt;Each system has different field names, date formats, location shapes and salary structures. If you want one clean table, you end up writing a small adapter per ATS, plus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;detecting which ATS a company uses,&lt;/li&gt;
&lt;li&gt;handling pagination and rate limits politely,&lt;/li&gt;
&lt;li&gt;deduplicating, and&lt;/li&gt;
&lt;li&gt;remembering which jobs you've already seen if you run it daily.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A shortcut
&lt;/h2&gt;

&lt;p&gt;I packaged all of this as an Apify Actor: &lt;strong&gt;&lt;a href="https://apify.com/swiftkit/company-jobs" rel="noopener noreferrer"&gt;Company Jobs&lt;/a&gt;&lt;/strong&gt;. You give it a list of companies (names, websites or careers URLs) and it returns one normalized row per open job from &lt;strong&gt;Workday, Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio, Teamtailor and Rippling&lt;/strong&gt;:&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;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Data 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;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"San Francisco; Mountain View"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryMin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;235000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryMax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;385000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ashby"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://jobs.ashbyhq.com/openai/…"&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;Two features I use most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Only new jobs since my last run"&lt;/strong&gt;: put it on a daily schedule and get just the new postings. Great for hiring alerts or "which of my target accounts just opened a data team?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Title, location and remote filters&lt;/strong&gt;, so you only pay for jobs you actually want.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's pay-per-result (about $1 per 1,000 jobs), and it only reads the public job boards that companies publish themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be a good citizen
&lt;/h2&gt;

&lt;p&gt;Whatever route you take:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stick to the official public endpoints above, not HTML scraping.&lt;/li&gt;
&lt;li&gt;Keep concurrency low and back off on 429s.&lt;/li&gt;
&lt;li&gt;Don't collect recruiter names or emails that some boards include. You don't need them for job data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If there's an ATS you'd like covered next, tell me in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with AI assistance; the endpoints, numbers and examples were tested before publishing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>javascript</category>
      <category>datascience</category>
      <category>career</category>
    </item>
  </channel>
</rss>
