<?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: Alen | SmartForm AI</title>
    <description>The latest articles on DEV Community by Alen | SmartForm AI (@alen_builds).</description>
    <link>https://dev.to/alen_builds</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%2F4151203%2F6e706cdf-6f6f-4746-9053-760e3adc0a9a.png</url>
      <title>DEV Community: Alen | SmartForm AI</title>
      <link>https://dev.to/alen_builds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alen_builds"/>
    <language>en</language>
    <item>
      <title>How to Handle Form Submissions in Node.js and Express (Without Writing Spam Logic)</title>
      <dc:creator>Alen | SmartForm AI</dc:creator>
      <pubDate>Tue, 10 Nov 2026 13:11:47 +0000</pubDate>
      <link>https://dev.to/alen_builds/how-to-handle-form-submissions-in-nodejs-and-express-without-writing-spam-logic-5fm0</link>
      <guid>https://dev.to/alen_builds/how-to-handle-form-submissions-in-nodejs-and-express-without-writing-spam-logic-5fm0</guid>
      <description>&lt;p&gt;The first Express contact form I shipped was about 140 lines. Multer for multipart parsing, &lt;code&gt;express-validator&lt;/code&gt; for the field checks, a honeypot field I hoped bots would fill, a Redis rate limiter, and a half-finished &lt;code&gt;isSpam&lt;/code&gt; heuristic that mostly flagged legitimate customers from Singapore.&lt;/p&gt;

&lt;p&gt;Then a bot filled the form 400 times in a night. It was a "SEO services" spam that slipped past because I'd hardcoded a keyword blocklist instead of classifying the message.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ &lt;strong&gt;The short version&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Post your Express form to a form backend endpoint and the spam filtering, intent classification, and notifications happen on their side. Or point a webhook at your Express app and receive already-processed submissions. Your Express code becomes "receive data" instead of "defend against spam."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What an express form backend actually costs you
&lt;/h2&gt;

&lt;p&gt;Here's the realistic dependency list for a production Express form endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm install express multer express-validator express-rate-limit dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the code that comes with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The version you'll write yourself. Every project, every time.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;multer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"multer"&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validationResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"express-validator"&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="n"&gt;rateLimit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"express-rate-limit"&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="n"&gt;upload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;multer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"uploads/"&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;formLimiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;windowMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;max&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="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/api/contact"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;formLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"attachment"&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="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEmail&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="nf"&gt;normalizeEmail&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="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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="nf"&gt;isLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&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="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"website"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"falsy"&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="c1"&gt;// the honeypot&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&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;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validationResult&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="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="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&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="n"&gt;length&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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&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="p"&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;// ...and now the part nobody can skip: deciding if it's spam.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;spamWords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"seo services"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"crypto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"backlinks"&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="n"&gt;spamWords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="c1"&gt;// silently drop&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&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="p"&gt;.&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;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&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="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"sales@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&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="n"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="p"&gt;}&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;That code works. It's also the part of your app most likely to break silently — the spam filter is a blunt instrument, the rate limiter needs Redis to be useful across instances, and multer writes attacker-controlled filenames to disk if you forget to sanitize them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Node.js form submission via POST from your Express route
&lt;/h2&gt;

&lt;p&gt;The endpoint is a plain public HTTP endpoint. No SDK, no special client library.&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="c1"&gt;// server.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"express"&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="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&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="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;express&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="p"&gt;)&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="no"&gt;FORM_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SMARTFORM_FORM_ID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SMARTFORM_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// workspace-level key (optional)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/api/contact"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&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;res&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="kd"&gt;const&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;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;body&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;message&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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"email and message required"&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&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="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Reserved fields — see the docs&lt;/span&gt;
    &lt;span class="n"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"https://yourapp.com/contact?sent=1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sb"&gt;`New contact form message from ${name || "someone"}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_gotcha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// honeypot — leave empty for real users&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="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="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&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="no"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"X-Api-Key"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;upstream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sb"&gt;`https://api.usesmartform.com/api/v1/f/${FORM_ID}`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&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="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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"upstream form service rejected the request"&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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;upstream&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="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;submissionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submission_id&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="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&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 things worth noting.&lt;/p&gt;

&lt;p&gt;First, the workspace API key in &lt;code&gt;X-Api-Key&lt;/code&gt; is optional. Without it, requests go through the same per-IP rate limit that protects browser-submitted forms (100 submissions per IP per hour). With it, server-to-server traffic skips the IP limit — which matters if your Express app sits behind a shared NAT or a corporate proxy.&lt;/p&gt;

&lt;p&gt;Second, the response comes back with &lt;code&gt;is_spam&lt;/code&gt; and &lt;code&gt;intent&lt;/code&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="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"submission_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"sub_9f2c1a8e4b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"is_spam"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"intent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"sales"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So you can branch on it if you want. Most people don't — they return &lt;code&gt;{ ok: true }&lt;/code&gt; to the browser and let the webhook handle the routing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why spam submissions don't cost you quota&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SmartForm AI's honeypot catches the obvious bot traffic before it reaches the AI classifier, and the classifier catches the rest. Neither path burns a submission from your monthly quota. That's the difference between a form backend and a raw POST endpoint — you're not renting the spam problem, you're forwarding it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Option 2: A nodejs contact form where the browser POSTs directly
&lt;/h2&gt;

&lt;p&gt;If your Express app serves a page with a contact form, you may not need a proxy route at all. Point the form's &lt;code&gt;action&lt;/code&gt; at the endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://api.usesmartform.com/api/v1/f/YOUR_FORM_ID"&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Honeypot: hidden from humans, tempting to bots --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"position:absolute;left:-9999px"&lt;/span&gt; &lt;span class="na"&gt;aria-hidden&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Leave this empty&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"_gotcha"&lt;/span&gt; &lt;span class="na"&gt;tabindex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"-1"&lt;/span&gt; &lt;span class="na"&gt;autocomplete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Where the browser lands after a successful submit --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"_next"&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://yourapp.com/contact?sent=1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your Express server never touches the form. The tradeoff: the endpoint URL is visible in the HTML (which is fine — &lt;code&gt;form_id&lt;/code&gt; is a random 8-character string, not a secret), and you lose the chance to add your own server-side logic to the request path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: Receive processed submissions via webhook (express webhook form handler)
&lt;/h2&gt;

&lt;p&gt;This is the pattern I use when the form data needs to end up somewhere in my app — creating a ticket, enriching a CRM record, posting to a chat channel. You configure a webhook URL on the form in the dashboard, and SmartForm AI POSTs the finished submission to your Express server.&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="c1"&gt;// routes/webhooks.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"express"&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="n"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"crypto"&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="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Router&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="c1"&gt;// Express needs the raw body to verify the HMAC signature.&lt;/span&gt;
&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"/webhooks/smartform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&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="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;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&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="s2"&gt;"X-SmartForm-Signature"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"sha256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SMARTFORM_WEBHOOK_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&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;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hex"&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="n"&gt;provided&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signature&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="o"&gt;/^&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&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="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;provided&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Buffer&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="n"&gt;provided&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Buffer&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="n"&gt;expected&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="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;valid&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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"invalid signature"&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="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;JSON&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="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"utf8"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;submission&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&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="n"&gt;submission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_spam&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Quietly archive. Don't page the on-call for spam.&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="p"&gt;}&lt;/span&gt;

    &lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`[${form.name}] ${submission.intent_label}: ${submission.ai_summary}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;express.raw()&lt;/code&gt; middleware is the detail people miss. If &lt;code&gt;express.json()&lt;/code&gt; runs first, it consumes and re-serializes the body, and the HMAC check fails because the bytes changed. Register the raw parser on the webhook route before any global JSON parser.&lt;/p&gt;

&lt;p&gt;The payload itself carries everything the AI pipeline produced:&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="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"submission.created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"submitted_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2026-11-01T09:14:22.481Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"form"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"a1b2c3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"form_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"kX9mN2pQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Website contact"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"submission"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"9f2c1a8e4b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"is_spam"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"spam_confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.04&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"intent_label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"sales"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"intent_confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"is_high_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"ai_summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Enterprise pricing inquiry from Acme Inc. for 50 seats."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"ip_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"203.0.113.9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"user_agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Mozilla/5.0 ..."&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;That &lt;code&gt;ai_summary&lt;/code&gt; and &lt;code&gt;intent_label&lt;/code&gt; are the payoff. A "route this to sales vs support" decision that would have been a string-matching guess in your own code is already made, on a model that reads the actual message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which pattern should you pick?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Just store the submission and notify yourself&lt;/td&gt;
&lt;td&gt;Browser POSTs straight to the endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom logic before the submission is recorded&lt;/td&gt;
&lt;td&gt;Express proxy route (Option 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Send the data into another system&lt;/td&gt;
&lt;td&gt;Webhook into your Express app (Option 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can also combine them: browser → Express proxy → endpoint, plus a webhook back into Express for the enrichment step. The proxy handles rate limiting and field normalization; the webhook gives you the AI verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part you don't have to write
&lt;/h2&gt;

&lt;p&gt;The reason I stopped maintaining my own spam filter wasn't that reCAPTCHA is bad — it's that spam detection is a moving target. The keyword list that worked last quarter now blocks two real customers a week. The honeypot that caught 90% of bots last year now catches 40%.&lt;/p&gt;

&lt;p&gt;SmartForm AI runs that classification server-side on every submission, and the part that matters for cost: &lt;strong&gt;spam doesn't count against your submission quota.&lt;/strong&gt; You can be as aggressive with the filter as you want without your bill going up, which is exactly what you can't say about a filter you host yourself, where every rejected request still costs you a CPU cycle.&lt;/p&gt;

&lt;p&gt;If you want to see the shape of the payloads before wiring anything up, the &lt;a href="https://usesmartform.com/playground" rel="noopener noreferrer"&gt;playground&lt;/a&gt; lets you fire a submission at the endpoint and read the response and webhook body back.&lt;/p&gt;

&lt;p&gt;For the Python side of the same problem — Django and Flask specifically — I wrote &lt;a href="https://usesmartform.com/blog/django-flask-contact-form-python" rel="noopener noreferrer"&gt;a guide to that too&lt;/a&gt;. And if you want to push these submissions somewhere other than your app, &lt;a href="https://usesmartform.com/blog/form-workflow-automation-webhooks" rel="noopener noreferrer"&gt;the webhook automation guide&lt;/a&gt; covers Slack, Notion, and Telegram.&lt;/p&gt;

</description>
      <category>node</category>
      <category>express</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Added a Contact Form to an Astro Site Without a Backend</title>
      <dc:creator>Alen | SmartForm AI</dc:creator>
      <pubDate>Tue, 27 Oct 2026 03:15:27 +0000</pubDate>
      <link>https://dev.to/alen_builds/i-added-a-contact-form-to-an-astro-site-without-a-backend-595b</link>
      <guid>https://dev.to/alen_builds/i-added-a-contact-form-to-an-astro-site-without-a-backend-595b</guid>
      <description>&lt;p&gt;I moved a client's marketing site to Astro a few weeks ago. Astro 7 was the reason — the Rust compiler and stable route caching cut the build from ~4 minutes to about 90 seconds. Fast builds were the whole point.&lt;/p&gt;

&lt;p&gt;Then I hit the one thing Astro can't do out of the box: &lt;strong&gt;accept a contact form submission.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the honest version of what I tried, what worked, and the two gotchas that cost me an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Astro forms trip people up
&lt;/h2&gt;

&lt;p&gt;Astro is component-first and ships almost zero client-side JavaScript. Most "add a form" tutorials assume a Node runtime. Three things follow from that, and they're the reason this feels different from a Next.js or Rails form:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A form has to POST somewhere.&lt;/strong&gt; The &lt;code&gt;action&lt;/code&gt; attribute is just a URL. There's no "submit to Astro" without an adapter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Astro Actions need a server adapter.&lt;/strong&gt; They're stable and genuinely type-safe, but they require &lt;code&gt;output: 'server'&lt;/code&gt; or &lt;code&gt;'hybrid'&lt;/code&gt; plus &lt;code&gt;@astrojs/vercel&lt;/code&gt; / &lt;code&gt;netlify&lt;/code&gt; / &lt;code&gt;cloudflare&lt;/code&gt; / &lt;code&gt;node&lt;/code&gt;. On the default &lt;code&gt;output: 'static'&lt;/code&gt; build, they don't run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No spam filtering is included.&lt;/strong&gt; Neither Actions nor a plain &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; ships with detection. That's on you.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I tried (and why I backed out)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Astro Actions.&lt;/strong&gt; Beautiful DX, end-to-end typed. But to handle one 3-field contact form I had to flip &lt;code&gt;output: 'static'&lt;/code&gt; to &lt;code&gt;'server'&lt;/code&gt;, add an adapter, manage secrets, and accept cold starts. My "static site" now had a server. That's a lot of surface area for a page that asks for a name, an email, and a message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Netlify Forms.&lt;/strong&gt; Five minutes, genuinely nice — but the site was on Vercel, so it was a non-starter. The lock-in matters more than the price tag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Turnstile + Workers.&lt;/strong&gt; Turnstile is a great CAPTCHA (free, accessible, no image grids). But to use it on Vercel I now maintain a Worker. I'd traded an adapter for a different server to maintain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually shipped:&lt;/strong&gt; a plain HTML form POSTing to an external endpoint. No adapter, no server, no secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The form
&lt;/h2&gt;

&lt;p&gt;This is the entire integration. &lt;code&gt;src/components/ContactForm.astro&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;form
  action="https://api.usesmartform.com/api/v1/f/YOUR_FORM_ID"
  method="POST"
&amp;gt;
  &amp;lt;label for="name"&amp;gt;Name&amp;lt;/label&amp;gt;
  &amp;lt;input id="name" name="name" type="text" required minlength="2" /&amp;gt;

  &amp;lt;label for="email"&amp;gt;Email&amp;lt;/label&amp;gt;
  &amp;lt;input id="email" name="email" type="email" required /&amp;gt;

  &amp;lt;label for="message"&amp;gt;Message&amp;lt;/label&amp;gt;
  &amp;lt;textarea id="message" name="message" rows="6" required minlength="10"&amp;gt;&amp;lt;/textarea&amp;gt;

  {/* Honeypot — bots fill it, humans never see it */}
  &amp;lt;input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off" /&amp;gt;

  {/* Where to send the browser after a successful submit */}
  &amp;lt;input type="hidden" name="_next" value="https://yoursite.com/thanks" /&amp;gt;

  &amp;lt;button type="submit"&amp;gt;Send Message&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop it in a page and you're done. Astro's whole pitch is zero JS by default, and a &lt;code&gt;method="POST"&lt;/code&gt; form navigates to the endpoint and back — the service returns a &lt;code&gt;302&lt;/code&gt; to your &lt;code&gt;_next&lt;/code&gt; URL. No client-side state, no hydration cost, no framework.&lt;/p&gt;

&lt;p&gt;It doesn't matter whether the surrounding page is an Astro component or a React/Vue/Svelte island. As long as the rendered DOM contains &lt;code&gt;&amp;lt;form action="..." method="POST"&amp;gt;&lt;/code&gt;, the submission lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #1: the &lt;code&gt;Accept&lt;/code&gt; header
&lt;/h2&gt;

&lt;p&gt;If you want an inline success message instead of a page redirect, use &lt;code&gt;fetch&lt;/code&gt;. The first version I wrote returned a 302, and &lt;code&gt;fetch&lt;/code&gt; followed it cross-origin into a CORS error that looked like a server outage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.usesmartform.com/api/v1/f/YOUR_FORM_ID&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// ← required&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;data&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;res&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="c1"&gt;// includes next_url&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Thanks — we'll get back to you shortly.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&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;Always send &lt;code&gt;Accept: application/json&lt;/code&gt;. You get JSON back (including &lt;code&gt;next_url&lt;/code&gt;) instead of a redirect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha #2: the honeypot needs three attributes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;display:none&lt;/code&gt; alone isn't enough. I had &lt;code&gt;style="display:none"&lt;/code&gt; and still saw bots post clean submissions, because they'd learned to skip hidden inputs. Adding &lt;code&gt;tabindex="-1"&lt;/code&gt; (keeps it out of keyboard nav) and &lt;code&gt;autocomplete="off"&lt;/code&gt; (keeps password managers out) closed the gap.&lt;/p&gt;

&lt;p&gt;To actually test the filter I removed the honeypot for a day, watched bot traffic arrive, then put it back and watched those same submissions get dropped server-side — silently, with no email and no quota deduction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spam: layer it, don't pick one
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Free&lt;/th&gt;
&lt;th&gt;User friction&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Honeypot field&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;The dumbest bots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Turnstile&lt;/td&gt;
&lt;td&gt;Yes (≤20 widgets)&lt;/td&gt;
&lt;td&gt;Almost none&lt;/td&gt;
&lt;td&gt;Browser-level bots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reCAPTCHA v3&lt;/td&gt;
&lt;td&gt;Yes (≤1M/mo)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Script bots, but flags real users sometimes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML filter (server-side)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Targeted / scripted spam&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What I ended up with: honeypot as the cheap first layer, a server-side classifier behind it, and Turnstile in front only on the demo-request form where a false positive costs us a lead.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I'd use Astro Actions instead
&lt;/h2&gt;

&lt;p&gt;If you already have &lt;code&gt;output: 'server'&lt;/code&gt; and an adapter, use Astro Actions. If you're tying submissions to a logged-in user, or the form writes to your database, Actions is the right tool. I'm not against it — I just didn't want to add a server to a static site for one contact form.&lt;/p&gt;

&lt;p&gt;It's also fine to mix. That site uses Actions for in-product feedback and an external endpoint for the marketing contact form. They're independent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;For a static Astro site with one marketing form, the whole thing should be ~20 lines of HTML and zero infrastructure. That's what I shipped. The hosted-service part is my own tool, so treat that section as biased — but the underlying pattern (a plain POST to an endpoint) works with any provider, including one you run yourself.&lt;/p&gt;

&lt;p&gt;I didn't want to write this as a sales pitch, but full disclosure: the external endpoint I used is my own product, SmartForm AI. The plain POST pattern works with any provider. If you want the full comparison matrix, I put the long-form version on my blog (linked in the comments).&lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Form Backend Pricing in 2026: What Actually Costs You Money</title>
      <dc:creator>Alen | SmartForm AI</dc:creator>
      <pubDate>Mon, 12 Oct 2026 18:55:36 +0000</pubDate>
      <link>https://dev.to/alen_builds/form-backend-pricing-in-2026-what-actually-costs-you-money-456l</link>
      <guid>https://dev.to/alen_builds/form-backend-pricing-in-2026-what-actually-costs-you-money-456l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version.&lt;/strong&gt; At 5,000 submissions/month, SmartForm AI Pro is $9.90 ($1.98 per 1K), Forminit Pro is $19 ($3.80 per 1K), Basin Pro is $40.83 ($8.17 per 1K), and Formspree's cheapest tier with webhooks is $30/month but caps at 2,000 submissions. Netlify Forms is genuinely free — if your site is hosted there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjvw4elybfv03es9400zc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjvw4elybfv03es9400zc.png" alt="pricing-comparison" width="799" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Form backend pricing pages have a specific problem: &lt;strong&gt;the headline number is almost never the number you pay.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I've seen this pattern repeat across a dozen static sites, so I went back and re-verified every major vendor's pricing page in October 2026. Several numbers that are quoted everywhere turned out to be badly outdated — one vendor had rebranded entirely, another had made its free tier unlimited, and a third no longer sells the tier names most comparison posts still reference.&lt;/p&gt;

&lt;p&gt;This is the corrected version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why re-verify pricing at all?
&lt;/h2&gt;

&lt;p&gt;Because form backend pricing churns hard. In the last six months:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Netlify&lt;/strong&gt; moved to credit-based plans (April 2026) and made Forms free and unlimited.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Getform rebranded to Forminit&lt;/strong&gt; (January 2026) with an entirely new pricing structure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Formspree&lt;/strong&gt; retired Bronze / Gold / Platinum in favor of Personal / Professional / Business, and moved AI spam filtering from a paid add-on into every paid tier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any comparison post older than that is going to mislead you. So this one has a date on it, and I'll note the verification date rather than pretending these numbers are permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR by volume
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your monthly volume&lt;/th&gt;
&lt;th&gt;Cheapest option with webhooks&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Under 200/mo&lt;/td&gt;
&lt;td&gt;SmartForm AI Free or FormSubmit (no webhooks)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 – 5,000/mo&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SmartForm AI Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$9.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5,000 – 50,000/mo&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SmartForm AI Business&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$19.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Over 50,000/mo&lt;/td&gt;
&lt;td&gt;Contact sales or self-host&lt;/td&gt;
&lt;td&gt;Negotiated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Real monthly cost comparison
&lt;/h2&gt;

&lt;p&gt;Cheapest plan that &lt;strong&gt;actually covers the volume&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Monthly submissions&lt;/th&gt;
&lt;th&gt;SmartForm AI&lt;/th&gt;
&lt;th&gt;Formspree&lt;/th&gt;
&lt;th&gt;Netlify Forms&lt;/th&gt;
&lt;th&gt;Basin&lt;/th&gt;
&lt;th&gt;Forminit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0&lt;/strong&gt; (Free, 200/mo)&lt;/td&gt;
&lt;td&gt;$0 (Free, 50/mo)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0&lt;/strong&gt;¹&lt;/td&gt;
&lt;td&gt;$0 (Free, 50/mo)&lt;/td&gt;
&lt;td&gt;$0 (Free, 100/mo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;200&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0&lt;/strong&gt; (Free, 200/mo)&lt;/td&gt;
&lt;td&gt;$15 (Personal)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0&lt;/strong&gt;¹&lt;/td&gt;
&lt;td&gt;$12.50 (Starter)&lt;/td&gt;
&lt;td&gt;$0 (Free — over cap)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1,000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$9.9&lt;/strong&gt; (Pro)&lt;/td&gt;
&lt;td&gt;$15 (over 200 cap)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0&lt;/strong&gt;¹&lt;/td&gt;
&lt;td&gt;$12.50 (over 250 cap)&lt;/td&gt;
&lt;td&gt;$19 (Pro)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5,000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$9.9&lt;/strong&gt; (Pro)&lt;/td&gt;
&lt;td&gt;$30 (Professional, only 2,000)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0&lt;/strong&gt;¹&lt;/td&gt;
&lt;td&gt;$40.83 (Pro)&lt;/td&gt;
&lt;td&gt;$19 (Pro)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;25,000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$19.9&lt;/strong&gt; (Business)&lt;/td&gt;
&lt;td&gt;$60 (Business, only 20,000)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0&lt;/strong&gt;¹&lt;/td&gt;
&lt;td&gt;$108.33 (Agency)&lt;/td&gt;
&lt;td&gt;$49 (Business, only 10,000)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;50,000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$19.9&lt;/strong&gt; (Business)&lt;/td&gt;
&lt;td&gt;$60 (Business, only 20,000)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0&lt;/strong&gt;¹&lt;/td&gt;
&lt;td&gt;Contact sales&lt;/td&gt;
&lt;td&gt;$99 (Volume)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;100,000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contact sales&lt;/td&gt;
&lt;td&gt;Contact sales&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0&lt;/strong&gt;¹&lt;/td&gt;
&lt;td&gt;Contact sales&lt;/td&gt;
&lt;td&gt;Contact sales&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;¹ Netlify Forms is free and unlimited &lt;strong&gt;on credit-based plans&lt;/strong&gt; (default since April 2026). Legacy plans still have the 100 submissions/month cap.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Netlify asterisk is doing a lot of work
&lt;/h3&gt;

&lt;p&gt;Netlify's Forms really are free and unlimited. But there are two conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You're on a &lt;strong&gt;credit-based plan&lt;/strong&gt; (the default now).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Your site is hosted on Netlify.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Condition 2 is the one that catches people. Netlify's build pipeline is what processes &lt;code&gt;data-netlify="true"&lt;/code&gt; on your form. Deploy that same site to Vercel or Cloudflare Pages and the form simply doesn't exist. It's not "free hosting" — it's "free if you already chose this host."&lt;/p&gt;

&lt;h2&gt;
  
  
  What each service actually charges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SmartForm AI
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Monthly&lt;/th&gt;
&lt;th&gt;Submissions&lt;/th&gt;
&lt;th&gt;Forms&lt;/th&gt;
&lt;th&gt;Seats&lt;/th&gt;
&lt;th&gt;AI filtering&lt;/th&gt;
&lt;th&gt;Webhooks&lt;/th&gt;
&lt;th&gt;API keys&lt;/th&gt;
&lt;th&gt;Audit log&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;spam only&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$9.9&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;spam + intent&lt;/td&gt;
&lt;td&gt;✅ HMAC + 3× retry&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Business&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$19.9&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;spam + intent&lt;/td&gt;
&lt;td&gt;✅ + custom rules&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;365 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest caveat: &lt;strong&gt;Free has no webhooks and no API keys.&lt;/strong&gt; It's a hosted inbox with spam filtering, not a programmable endpoint. If you need to push submissions into a CRM, you start at $9.90. Pro includes AI filtering &lt;em&gt;and&lt;/em&gt; webhooks at the same price, which is the part that makes it competitive.&lt;/p&gt;

&lt;p&gt;At the cap, submissions are refused with &lt;code&gt;HTTP 402&lt;/code&gt; and &lt;code&gt;error_code: PLAN_QUOTA_EXCEEDED&lt;/code&gt;. No auto-upgrade, no surprise overage invoice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Formspree
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Annual&lt;/th&gt;
&lt;th&gt;Monthly&lt;/th&gt;
&lt;th&gt;Submissions&lt;/th&gt;
&lt;th&gt;AI spam filter&lt;/th&gt;
&lt;th&gt;Webhooks&lt;/th&gt;
&lt;th&gt;File uploads&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;✅ basic&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;0 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Personal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;✅ basic&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Professional&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;td&gt;2,000&lt;/td&gt;
&lt;td&gt;✅ + advanced control&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;5 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Business&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$60&lt;/td&gt;
&lt;td&gt;$90&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;✅ + custom rules&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things that have changed and that most posts still get wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AI spam filtering is no longer a paid add-on.&lt;/strong&gt; Formshield ML ships on every paid plan. If you see a "$9/mo AI add-on" in a comparison, it's out of date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no 5,000-submission tier.&lt;/strong&gt; Professional caps at 2,000; the next step is Business at 20,000 for $60/mo annual. So if you need 5,000/mo with webhooks, you pay $60 for a tier with 4× the volume you need.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What genuinely &lt;em&gt;is&lt;/em&gt; gated: webhooks, autoresponders, and API access all start at Professional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Netlify Forms
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Monthly&lt;/th&gt;
&lt;th&gt;Submissions&lt;/th&gt;
&lt;th&gt;Spam filtering&lt;/th&gt;
&lt;th&gt;Cross-platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Free&lt;/strong&gt; (credit)&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Honeypot + reCAPTCHA&lt;/td&gt;
&lt;td&gt;❌ Netlify only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Personal&lt;/strong&gt; (credit)&lt;/td&gt;
&lt;td&gt;$9/seat&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Honeypot + reCAPTCHA&lt;/td&gt;
&lt;td&gt;❌ Netlify only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Pro&lt;/strong&gt; (credit)&lt;/td&gt;
&lt;td&gt;$20/seat&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unlimited&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Honeypot + reCAPTCHA&lt;/td&gt;
&lt;td&gt;❌ Netlify only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Legacy plans&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Varies&lt;/td&gt;
&lt;td&gt;100/mo&lt;/td&gt;
&lt;td&gt;Honeypot + reCAPTCHA&lt;/td&gt;
&lt;td&gt;❌ Netlify only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No per-seat multiplier on form submissions — you pay Netlify for hosting, not for forms. Webhook delivery goes through Netlify Functions and consumes credits.&lt;/p&gt;

&lt;p&gt;The real cost isn't the money, it's the lock-in: moving hosts means rebuilding the form, and historical submission data lives in Netlify's backend (manual CSV export to get it out).&lt;/p&gt;

&lt;h3&gt;
  
  
  Basin
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Annual&lt;/th&gt;
&lt;th&gt;Submissions&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;AI features&lt;/th&gt;
&lt;th&gt;Zapier&lt;/th&gt;
&lt;th&gt;Webhooks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;100 MB&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Starter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$12.50&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;500 MB&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Growth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$24.17&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;2 GB&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$40.83&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;td&gt;✅ AI lead agent&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$108.33&lt;/td&gt;
&lt;td&gt;25,000&lt;/td&gt;
&lt;td&gt;50 GB&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Basin has the best Zapier / Make / n8n story here, and its &lt;strong&gt;overage behavior is the friendliest&lt;/strong&gt;: forms keep working past your cap and you're billed at tiered rates starting around $5.25 per 1,000 extra submissions.&lt;/p&gt;

&lt;p&gt;The catch is the AI features start at Pro ($40.83/mo), and 25,000 submissions costs $108.33 — 5× SmartForm AI Business for half the volume.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forminit (formerly Getform)
&lt;/h3&gt;

&lt;p&gt;Getform rebranded to &lt;strong&gt;Forminit on 14 January 2026&lt;/strong&gt; — same team, same product, not an acquisition. Existing &lt;code&gt;getform.io&lt;/code&gt; endpoints still work.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Monthly&lt;/th&gt;
&lt;th&gt;Submissions&lt;/th&gt;
&lt;th&gt;Forms&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;AI spam filter&lt;/th&gt;
&lt;th&gt;Webhooks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;100 MB&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$19&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1 GB&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Business&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$49&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;5 GB&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Volume&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$99&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;10 GB&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Genuinely nice: unlimited submission archive on every plan (including Free), typed server-side validation via Form Blocks, and both public and API-key-protected submission modes.&lt;/p&gt;

&lt;p&gt;But the limit behavior is the harshest in this comparison — Forminit's docs state that &lt;strong&gt;submissions sent while a form is paused are not stored or queued&lt;/strong&gt;. At your cap, your form stops and the submissions are gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost per 1,000 submissions
&lt;/h2&gt;

&lt;p&gt;The cleanest way to see the tier gaps:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Monthly $ / volume&lt;/th&gt;
&lt;th&gt;Cost per 1K&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SmartForm AI Free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0 / 200&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SmartForm AI Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$9.9 / 5,000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1.98&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SmartForm AI Business&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$19.9 / 50,000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.40&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Netlify Forms&lt;/strong&gt; (credit plan)&lt;/td&gt;
&lt;td&gt;$0 / unlimited&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;FormSubmit&lt;/strong&gt; (free tier)&lt;/td&gt;
&lt;td&gt;$0 / unlimited&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;FormSubmit&lt;/strong&gt; (paid credits)&lt;/td&gt;
&lt;td&gt;$100 / 1,000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$100.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Forminit Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$19 / 5,000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$3.80&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Formspree Business&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$60 / 20,000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$3.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Basin Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$40.83 / 5,000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$8.17&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Formspree Professional&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$30 / 2,000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$15.00&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three $0 rows all come with real constraints: SmartForm AI Free caps at 200/mo with no webhooks, Netlify requires Netlify hosting, and FormSubmit is email-only with no filtering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Of the paid options that include webhooks, SmartForm AI Pro at $1.98 per 1K is the cheapest&lt;/strong&gt; — about half of Formspree Business, a fifth of Basin Pro, and nearly 8× cheaper than Formspree Professional.&lt;/p&gt;

&lt;h3&gt;
  
  
  On FormSubmit's paid model
&lt;/h3&gt;

&lt;p&gt;Worth calling out because it's unusual: &lt;strong&gt;FormSubmit has no monthly subscription plans.&lt;/strong&gt; You buy credits at &lt;strong&gt;$0.10 per submission&lt;/strong&gt; (cheaper at volume), credits &lt;strong&gt;never expire&lt;/strong&gt;, and &lt;strong&gt;spam doesn't cost a credit&lt;/strong&gt;. Email notifications cost +1 credit and are off by default. Forms are unlimited.&lt;/p&gt;

&lt;p&gt;At 1,000 submissions/month that's about &lt;strong&gt;$100/month&lt;/strong&gt;, which makes it a pay-as-you-go safety valve rather than a real form backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when you hit the limit
&lt;/h2&gt;

&lt;p&gt;This is the detail most comparison posts skip, and it's the one that decides whether a price spike costs you a few dollars or a few leads:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Behavior at the cap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SmartForm AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Submissions &lt;strong&gt;refused&lt;/strong&gt; with HTTP 402. No auto-upgrade, no overage billing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Formspree&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stops accepting submissions, prompts an upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Netlify Forms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nothing — unlimited on credit plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Basin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Keeps accepting&lt;/strong&gt; and bills the overage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Forminit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Forms &lt;strong&gt;pause and the submissions are discarded&lt;/strong&gt; (not stored or queued)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FormSubmit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free tier keeps accepting; paid credits just run out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If losing a lead silently is worse for your business than showing an error, that ranking matters more than the price column.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommendations by scenario
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Personal blog, under 200/mo&lt;/strong&gt; → SmartForm AI Free ($0) or FormSubmit ($0). Both free; SmartForm AI adds filtering and a dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SaaS site, 200 – 5,000/mo, needs webhooks into a CRM&lt;/strong&gt; → SmartForm AI Pro ($9.90). The cheapest place webhooks and AI filtering exist together. Formspree's equivalent is $30/mo monthly and caps at 2,000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High-volume B2B or e-commerce, 5,000 – 50,000/mo&lt;/strong&gt; → SmartForm AI Business ($19.90), which includes 10 seats and a 365-day audit log. Basin charges $108.33 for 25,000; Forminit charges $49 for 10,000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over 50,000/mo&lt;/strong&gt; → Talk to sales or self-host. At that volume you want SLA terms and compliance guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part pricing pages never mention
&lt;/h2&gt;

&lt;p&gt;The line item that actually costs you money isn't the subscription — it's &lt;strong&gt;the leads your filter throws away&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If a real customer gets classified as spam, they're gone. They won't submit twice. That's why SmartForm AI exposes &lt;code&gt;spam_confidence&lt;/code&gt; and &lt;code&gt;intent_confidence&lt;/code&gt; in the webhook payload: you can set thresholds per form rather than accepting a global default, which is how you keep a false positive from eating your highest-value inquiry while still blocking obvious junk.&lt;/p&gt;

&lt;p&gt;The other hidden cost is engineering time. If your backend has no ML filtering, someone on your team is reading spam — setting up honeypots once, then triaging forever. A couple of hours a month that never shows up on an invoice but never goes away either.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Does Formspree still charge $9/mo for AI filtering?&lt;/strong&gt;&lt;br&gt;
No. That add-on is gone — basic ML filtering (Formshield) is included on every paid plan. What costs extra is the &lt;em&gt;tier&lt;/em&gt;: webhooks, autoresponders, and API access require Professional ($30/mo monthly) or above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is Netlify Forms really unlimited now?&lt;/strong&gt;&lt;br&gt;
On credit-based plans, yes — Netlify's docs state "For credit-based plans, Forms are free and unlimited." The 100 submissions/month cap still applies to legacy plans. The catch is you must be hosting on Netlify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is Getform the same as Forminit?&lt;/strong&gt;&lt;br&gt;
Yes. Getform was renamed Forminit on 14 January 2026. Same team, same product, existing endpoints still work. Pricing changed, so older comparisons are wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: What happens if I exceed my SmartForm AI limit?&lt;/strong&gt;&lt;br&gt;
Submissions are refused with &lt;code&gt;HTTP 402&lt;/code&gt; and &lt;code&gt;error_code: PLAN_QUOTA_EXCEEDED&lt;/code&gt;. There's no automatic upgrade and no overage billing — you upgrade manually. I find this preferable to a surprise invoice, but it's a real difference from Basin, which bills overages instead of blocking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Which is cheapest for a one-time landing page?&lt;/strong&gt;&lt;br&gt;
FormSubmit, at $0, with no account setup. If you expect more than a handful of submissions or want any filtering, use SmartForm AI Free — also $0, with a dashboard and AI spam filtering.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Pricing verified against vendor websites in October 2026. Form backends move fast — if you spot something that's drifted, drop a comment and I'll update it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>jamstack</category>
      <category>staticsite</category>
      <category>sass</category>
    </item>
    <item>
      <title>5 Formspree Alternatives Worth Switching to in 2026 (An Honest Comparison)</title>
      <dc:creator>Alen | SmartForm AI</dc:creator>
      <pubDate>Mon, 05 Oct 2026 02:33:43 +0000</pubDate>
      <link>https://dev.to/alen_builds/5-formspree-alternatives-worth-switching-to-in-2026-an-honest-comparison-257j</link>
      <guid>https://dev.to/alen_builds/5-formspree-alternatives-worth-switching-to-in-2026-an-honest-comparison-257j</guid>
      <description>&lt;p&gt;Last year I moved a yoga studio onto Astro. Their contact form ran on Formspree's free tier, and they burned through all 50 monthly submissions in the first week — almost entirely Russian spam.&lt;/p&gt;

&lt;p&gt;Nobody noticed. Not the studio owner, not the visitors. Formspree doesn't throw an error; it just stops accepting. For the remaining three weeks that "Book a session" form didn't exist.&lt;/p&gt;

&lt;p&gt;Formspree's ads say $8/mo. Another client's invoice was $90. Not their fault — I just hadn't understood how their pricing is structured.&lt;/p&gt;

&lt;p&gt;(For the record, the yoga studio ended up on Forminit. Not because it's the best fit — because she needed file uploads for class schedules, and at 40 submissions a month nothing else mattered. Sometimes the right answer is the boring one.)&lt;/p&gt;

&lt;p&gt;I'm Alen, and I build SmartForm AI, a form backend for static sites. So this post is biased by construction. I'll try to be straight about where each competitor genuinely wins, and I'll tell you what's wrong with my own product too. All pricing below was re-checked against vendor pages in October 2026.&lt;/p&gt;

&lt;p&gt;How I tested: one real 5-field contact form on a static site, then roughly 500 synthetic submissions pushed through each service, 30% of them spam. I cared most about three things — what happens to a submission when the filter is wrong, what happens when you cross the cap, and how long the migration actually takes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Formspree math first
&lt;/h2&gt;

&lt;p&gt;Free is 50 submissions/month. One decent landing page burns that in a week.&lt;/p&gt;

&lt;p&gt;Personal is &lt;strong&gt;$10/mo annual or $15/mo monthly&lt;/strong&gt; for 200 submissions. But webhooks, autoresponders, and API access are all excluded — those start at &lt;strong&gt;Professional, $20/mo annual or $30/mo monthly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's what most comparison posts get wrong: &lt;strong&gt;there is no 5,000-submission tier.&lt;/strong&gt; Professional caps at 2,000, and the next step jumps to Business at 20,000 for $60/mo annual or $90/mo monthly. So if you need 5,000/mo with webhooks, you're paying for a tier built for 20,000.&lt;/p&gt;

&lt;p&gt;It also still has no AI in 2026 — no intent classification, no summaries, no lead scoring. You can bolt on Zapier plus an LLM, but that's two more bills and something else to debug at 2 AM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Netlify Forms is free, but only in one situation
&lt;/h2&gt;

&lt;p&gt;Netlify moved to credit-based plans in April 2026, and the docs are direct about it: &lt;em&gt;"For credit-based plans, Forms are free and unlimited."&lt;/em&gt; The old 100 submissions/month cap only applies to legacy plans.&lt;/p&gt;

&lt;p&gt;This is the cheapest price in the whole list, so here's the condition that matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;data-netlify="true"&lt;/code&gt; is processed by Netlify's build pipeline.&lt;/strong&gt; Deploy that same site to Vercel or Cloudflare Pages and nothing reads the attribute — the form simply doesn't exist. So this isn't "free hosting." It's "free if you already picked Netlify as your host."&lt;/p&gt;

&lt;p&gt;No AI, just honeypot and reCAPTCHA. Submission history lives in Netlify's backend, and getting it out means a manual CSV download.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"contact"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt; &lt;span class="na"&gt;data-netlify=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Basin owns the Zapier ecosystem
&lt;/h2&gt;

&lt;p&gt;Basin's whole reputation is Zapier / Make / n8n integration — 100+ native apps, and "form → Notion → Slack → CRM" is a turnkey setup. Pricing runs Starter $12.50, Growth $24.17, Pro $40.83, Agency $108.33.&lt;/p&gt;

&lt;p&gt;The thing I like most: &lt;strong&gt;overages don't cut you off.&lt;/strong&gt; Traffic spikes, submissions keep arriving, and you're billed on a tiered rate (about $5.25 per 1,000 extra on Pro, dropping to $2 past 100K). That's far nicer than a hard HTTP 402, and for an indie dev who can't forecast launch-week traffic, not losing leads is worth real money.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The free tier deletes your data after 30 days.&lt;/strong&gt; I missed this on first setup and a client's test inbox emptied itself out a month later. The 100 MB storage and 30-day window are easy to skim past because the signup flow never mentions them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI features start at Pro ($40.83/mo).&lt;/strong&gt; Everything below that is honeypot and captcha, no ML classification. If you're comparing Basin to a $9.90 competitor on price alone, remember you're comparing different products.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I only ran Basin's 14-day trial and didn't go deep on the AI lead agent, so take that part from their docs rather than my testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forminit got renamed, and it drops submissions at the cap
&lt;/h2&gt;

&lt;p&gt;Getform rebranded to &lt;strong&gt;Forminit on January 14, 2026&lt;/strong&gt; — same team, not an acquisition. Old &lt;code&gt;getform.io&lt;/code&gt; endpoints still work, so if you already use Getform, you're already a Forminit customer.&lt;/p&gt;

&lt;p&gt;Current tiers: Free $0 (100 submissions, 1 form), Pro $19 (5,000), Business $49 (10,000), Volume $99 (50,000).&lt;/p&gt;

&lt;p&gt;It's technically stronger than most people assume. &lt;strong&gt;Form Blocks do server-side typed validation&lt;/strong&gt; — E.164 phone numbers, email format, country codes — instead of untyped key-value pairs. Every plan including Free has an unlimited submission archive with no auto-deletion.&lt;/p&gt;

&lt;p&gt;But its limit behavior is the harshest I've seen: &lt;strong&gt;when a form hits its cap it pauses, and submissions sent while paused are not stored or queued.&lt;/strong&gt; They're gone. That's straight from their docs. Basin bills you and keeps going, Netlify has no cap at all, Forminit discards silently. If losing a lead quietly is worse for your business than showing an error page, that matters.&lt;/p&gt;

&lt;p&gt;No AI filtering either — server-side validation and honeypot only. Pro at $19 for 5,000 submissions is double SmartForm AI's Pro for the same volume.&lt;/p&gt;

&lt;h2&gt;
  
  
  FormSubmit is more capable than I gave it credit for
&lt;/h2&gt;

&lt;p&gt;formsubmit.co is the simplest option: no signup, submissions land in your email.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"https://formsubmit.co/your@email.com"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unlimited forms, unlimited submissions, and — this is the part I got wrong when I first wrote this post — &lt;strong&gt;every form comes with reCAPTCHA using machine learning&lt;/strong&gt; to separate humans from bots. You can turn it off per form with &lt;code&gt;_captcha="false"&lt;/code&gt;, though their docs advise against it.&lt;/p&gt;

&lt;p&gt;It also does native file uploads (10MB per submission), &lt;code&gt;_autoresponse&lt;/code&gt; for instant confirmations, and &lt;code&gt;_webhook&lt;/code&gt; for delivery. That's a more complete feature set than the "email pipe" reputation suggests.&lt;/p&gt;

&lt;p&gt;What's actually missing is the interface, not the features. There's no web dashboard. If you miss an email you can pull the archive through their free API — &lt;strong&gt;five times a day&lt;/strong&gt; — and they retain submissions for &lt;strong&gt;30 days&lt;/strong&gt;. That last number is the same trap as Basin's free tier, in a place you'd never think to look for it.&lt;/p&gt;

&lt;p&gt;The paid tier is a separate product on a separate domain. &lt;a href="https://formsubmit.site" rel="noopener noreferrer"&gt;formsubmit.site&lt;/a&gt; sells &lt;strong&gt;credits&lt;/strong&gt;, not subscriptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$0.10 per submission&lt;/strong&gt; to start, dropping to $0.07 at volume&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Credits never expire&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spam doesn't cost a credit&lt;/strong&gt; — only clean submissions count&lt;/li&gt;
&lt;li&gt;Email notifications add +1 credit, off by default&lt;/li&gt;
&lt;li&gt;Unlimited forms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;1,000 submissions/month runs about $80–$100, more than any other service in this list. It's a pay-as-you-go safety valve rather than a primary backend. Its dashboard lives at &lt;code&gt;app.formsubmit.site&lt;/code&gt; — so if someone told you FormSubmit "has a dashboard," that's the paid product, not the free one.&lt;/p&gt;

&lt;h2&gt;
  
  
  SmartForm AI — including the parts I don't like
&lt;/h2&gt;

&lt;p&gt;I said this post is biased, so here are my product's problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The free tier has no webhooks and no API keys.&lt;/strong&gt; That one turns some developers away immediately. Free is a hosted inbox with spam filtering, not a programmable endpoint — if you need to push submissions into a CRM, you're starting at $9.9.&lt;/p&gt;

&lt;p&gt;Others: Pro is single-seat, so bringing a teammate into the same workspace means buying Business ($19.9, 10 seats). Audit logs keep 30 days on Pro and 365 on Business, and Free has none. Native file uploads don't exist yet — it's on the roadmap.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Submissions&lt;/th&gt;
&lt;th&gt;Forms&lt;/th&gt;
&lt;th&gt;Seats&lt;/th&gt;
&lt;th&gt;AI&lt;/th&gt;
&lt;th&gt;Webhooks&lt;/th&gt;
&lt;th&gt;API keys&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Free&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Spam only&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$9.9&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Spam + intent + summary&lt;/td&gt;
&lt;td&gt;✅ + HMAC&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Business&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$19.9&lt;/td&gt;
&lt;td&gt;50,000&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;All of Pro&lt;/td&gt;
&lt;td&gt;✅ + custom rules&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Past your cap it's a hard refusal: &lt;code&gt;HTTP 402&lt;/code&gt; with &lt;code&gt;error_code: PLAN_QUOTA_EXCEEDED&lt;/code&gt;, no auto-upgrade, no overage billing.&lt;/p&gt;

&lt;p&gt;The upside is that AI classification is on every paid tier, each submission gets a one-line summary, and webhooks are HMAC-SHA256 signed with 3 retries on failure. Integration is a one-line &lt;code&gt;action&lt;/code&gt; change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"https://api.usesmartform.com/api/v1/f/YOUR_FORM_ID"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Formspree's hidden fields (&lt;code&gt;_subject&lt;/code&gt;, &lt;code&gt;_gotcha&lt;/code&gt;, &lt;code&gt;_next&lt;/code&gt;) are understood natively, so migrating doesn't touch your HTML. The webhook payload adds a few fields on top of Formspree's:&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;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"submission.created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"submission"&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;"data"&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;"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;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"is_spam"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"spam_confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"intent_label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high-value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"intent_confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.87&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"is_high_value"&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;"ai_summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Enterprise customer asking about volume pricing for 50+ seats."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature header is &lt;code&gt;X-SmartForm-Signature: sha256=&amp;lt;hex&amp;gt;&lt;/code&gt;, HMAC-SHA256 over the raw body.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers together
&lt;/h2&gt;

&lt;p&gt;At 5,000 submissions/month with webhooks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Cheapest tier ≥ 5,000/mo&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Netlify Forms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Netlify-hosted only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SmartForm AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$9.9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pro — 5,000, AI included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Forminit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$19&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pro — 5,000, no AI filtering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Basin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$40.83&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pro — 5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Formspree&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Business — 20,000 (4× headroom)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FormSubmit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$80–100&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pay-per-credit, 1,000 is already $80+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Per 1,000 submissions that's $1.98 for SmartForm AI Pro, $0.40 for Business, $3.80 for Forminit Pro, and $8.17 for Basin Pro. Formspree's $60 Business tier works out to $3.00 per 1,000 — but it's a 20,000-submission tier, and there is no 5,000-submission equivalent to divide by.&lt;/p&gt;

&lt;p&gt;But price tables hide one thing that decides whether a spike costs you money or a lead: &lt;strong&gt;what each vendor does at the cap.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Behavior at the limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SmartForm AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Refuses submissions, HTTP 402, no auto-upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Basin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Keeps accepting, bills the overage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Netlify Forms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unlimited — nothing happens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Forminit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Form pauses, submissions discarded, unrecoverable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Formspree&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stops accepting, prompts an upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FormSubmit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free tier continues; paid credits run out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Migrating takes about 15 minutes
&lt;/h2&gt;

&lt;p&gt;If you decide to switch, it's three steps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sign up at &lt;a href="https://usesmartform.com/signup" rel="noopener noreferrer"&gt;usesmartform.com/signup&lt;/a&gt;, create a form, copy the 8-character &lt;code&gt;form_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Change the &lt;code&gt;action&lt;/code&gt; URL&lt;/li&gt;
&lt;li&gt;Send a test submission and confirm your webhook fires
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- &amp;lt;form action="https://formspree.io/f/YOUR_FORMSPREE_ID" method="POST"&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ &amp;lt;form action="https://api.usesmartform.com/api/v1/f/YOUR_FORM_ID" method="POST"&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 3 is the one people skip, and it's the one that matters. Fire a test submission and confirm the payload actually lands in your CRM before you delete anything on the old side — a 200 response from the form endpoint doesn't mean the webhook delivered. Check the signature header matches what your handler expects, or you'll silently accept forged payloads later.&lt;/p&gt;

&lt;p&gt;One caveat up front: SmartForm AI can't bulk-import historical submissions from Formspree. Export to CSV before you decommission if you need the old data, and treat that as the only genuinely irreversible part of the whole move.&lt;/p&gt;




&lt;p&gt;That's it — I have a webhook bug to go fix. If you're on the Formspree free tier, go look at this month's submission count. &lt;strong&gt;Past 40, start shopping&lt;/strong&gt;, because going over the limit is usually silent. Happy to argue about any of this in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pricing checked against vendor sites in October 2026. If something's drifted, say so and I'll update it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>jamstack</category>
      <category>formspree</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Adding a Contact Form to a Static Site in 2026: 5 Approaches I Actually Tried (and What I'd Build Differently Today</title>
      <dc:creator>Alen | SmartForm AI</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/alen_builds/adding-a-contact-form-to-a-static-site-in-2026-5-approaches-i-actually-tried-and-what-id-build-1boa</link>
      <guid>https://dev.to/alen_builds/adding-a-contact-form-to-a-static-site-in-2026-5-approaches-i-actually-tried-and-what-id-build-1boa</guid>
      <description>&lt;p&gt;If you've ever shipped a JAMstack site — Next.js, Astro, Hugo, Gatsby, Eleventy — you've hit the same wall I have: &lt;strong&gt;forms don't fit the static model&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No server. No PHP. No Node runtime. Just HTML, CSS, and JavaScript being served from a CDN. And then someone asks: &lt;em&gt;"Can you add a contact form?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Over the past year I've built contact forms for six different static sites — a personal blog, two client marketing sites, a SaaS landing page, an open-source project page, and a documentation portal. I went through every approach I could find before settling on one. This post is the field report — what worked, what broke, and what I'd do differently today.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A note on bias before we start&lt;/strong&gt;: I'm the developer behind &lt;a href="https://usesmartform.com" rel="noopener noreferrer"&gt;SmartForm AI&lt;/a&gt;, an AI-augmented form backend for static sites. I'll mention it once at the bottom of this post because it directly answers a problem I couldn't solve with the other four approaches. Everything before that is vendor-agnostic. If you only want the comparison, scroll to the matrix and skip the rest.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The problem, stated honestly
&lt;/h2&gt;

&lt;p&gt;A static contact form is deceptively simple on the surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the second you press "deploy", five real questions show up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where does the submission go?&lt;/strong&gt; (Email? Database? Slack?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do I stop spam?&lt;/strong&gt; (CAPTCHA? Honeypot? AI?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do I attach files?&lt;/strong&gt; (Resume uploads, screenshots)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do I stay notified?&lt;/strong&gt; (Email? Slack? Webhook to my CRM?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens when traffic spikes?&lt;/strong&gt; (Someone posts my form on Reddit and 5,000 bots show up in 10 minutes)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every approach below answers these differently. Let's walk through them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Approach 1: Platform-native forms (Netlify Forms / Cloudflare Forms)
&lt;/h2&gt;

&lt;p&gt;The path of least resistance. If you're already deploying on Netlify or Cloudflare Pages, you can usually turn on form handling with a single attribute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"contact"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt; &lt;span class="na"&gt;data-netlify=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Netlify captures submissions, gives you a UI in the dashboard, and even supports webhooks.&lt;/p&gt;

&lt;h3&gt;
  
  
  What works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Zero configuration.&lt;/li&gt;
&lt;li&gt;Spam filtering included (basic honeypot).&lt;/li&gt;
&lt;li&gt;Free tier is generous for hobby projects.&lt;/li&gt;
&lt;li&gt;Works with any static-site generator that produces HTML.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What breaks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vendor lock-in.&lt;/strong&gt; Migrating your static site off Netlify also means rewriting your form backend. I learned it the hard way on a Gatsby project when the build started failing after a Gatsby 4 → 5 upgrade and I had to move to Vercel. Forms broke silently. Submissions just stopped appearing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing model has changed twice in two years.&lt;/strong&gt; Netlify switched to credit-based plans in late 2025 — under the new model, Forms submissions are free and unlimited across all tiers, but legacy customers are still being metered on per-site limits. If you're reading tutorials written before 2025, double-check the actual current numbers — most of them are out of date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No AI.&lt;/strong&gt; Spam filtering is the standard honeypot plus Cloudflare Turnstile. Residential-proxy bot traffic that fills real-looking fields will sail through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lead intelligence.&lt;/strong&gt; Every submission is just rows in a UI. No classification, no AI summary, no Slack routing based on intent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Verdict
&lt;/h3&gt;

&lt;p&gt;Best for: tiny personal sites already deployed on Netlify. Worst for: any business site where form data is revenue.&lt;/p&gt;




&lt;h2&gt;
  
  
  Approach 2: SaaS form backends (Formspree, Web3Forms, Basin, Forminit)
&lt;/h2&gt;

&lt;p&gt;The de-facto option for most static sites today. You point your HTML form at a third-party endpoint and they handle storage, email, spam, and a dashboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"https://formspree.io/f/YOUR_FORM_ID"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via fetch for a SPA-style experience:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://formspree.io/f/YOUR_FORM_ID&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&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;h3&gt;
  
  
  What works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Portable.&lt;/strong&gt; Works on Vercel, Netlify, Cloudflare, GitHub Pages, raw S3, anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mature spam filtering.&lt;/strong&gt; Most services now offer reCAPTCHA, honeypot, or ML-based filtering (Formspree's "Formshield", Basin's reCAPTCHA + content filter) out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks and Zapier-style integrations&lt;/strong&gt; for routing to Slack, CRMs, etc. This is the killer feature for small teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decent free tiers&lt;/strong&gt; (Formspree 50/mo, Web3Forms 250/mo, Basin 50/mo).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What breaks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free-tier cliff, vendor by vendor.&lt;/strong&gt; Formspree jumps from $0 (50 subs) to $10/month (200 subs) on the Personal plan, then $20/month (2,000) on Professional. Web3Forms' free tier is more generous at 250/mo but its paid plans sit at a similar price-per-submission ratio. Basin is the priciest of the three: Starter is $12.50/mo for 250 submissions and Pro is $40.83/mo for 5,000 — roughly 4× more expensive than Formspree Pro for the same volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File uploads vary wildly.&lt;/strong&gt; Formspree includes 1GB / 5GB / 10GB on its paid tiers (none on Free). Web3Forms and Basin include file storage on paid tiers only. &lt;strong&gt;Getform rebranded to Forminit in January 2026&lt;/strong&gt; and is the only one of these that bundles 100MB of file storage on the Free tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lead intelligence.&lt;/strong&gt; Every submission lands in your inbox or dashboard as raw rows. If you want to know "which of these 80 messages are actually sales inquiries worth following up today?", you read them manually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits and burst behavior differ across vendors and aren't always published.&lt;/strong&gt; A friend hit a Web3Forms wall during a Product Hunt launch with no published threshold to debug against. Plan for a quiet week before any launch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Verdict
&lt;/h3&gt;

&lt;p&gt;Best for: most personal sites, side projects, low-volume marketing pages. Worst for: anything where form content needs classification or where free-tier → paid is too steep.&lt;/p&gt;




&lt;h2&gt;
  
  
  Approach 3: Serverless functions (Cloudflare Workers, Vercel Functions, AWS Lambda)
&lt;/h2&gt;

&lt;p&gt;The "real engineer" answer. Spin up a serverless function, accept the submission, store it, email it. Full control.&lt;/p&gt;

&lt;p&gt;A minimal Cloudflare Worker:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Method not allowed&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;405&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;data&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;request&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="c1"&gt;// Honeypot check&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_gotcha&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Send email via Mailgun&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.mailgun.net/v3/yourdomain.com/messages&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Basic &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;btoa&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MAILGUN_KEY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&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;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;noreply@yourdomain.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;you@yourdomain.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`New contact from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&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;Content-Type&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;application/json&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total control.&lt;/strong&gt; Your logic, your dependencies, your database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheap at scale.&lt;/strong&gt; Cloudflare Workers' free tier is 100k requests/day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No vendor lock-in.&lt;/strong&gt; The function is yours to deploy anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency.&lt;/strong&gt; Edge functions run geographically close to the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What breaks (and this is the long list)
&lt;/h3&gt;

&lt;p&gt;I built this exact setup for a client's marketing site. Here's what I learned:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spam filtering is now your problem.&lt;/strong&gt; Honeypots catch the dumbest bots, but the moment you deploy on a real domain you get targeted bots that read your source code, fill real-looking fields, and bypass honeypots. You'll be reading raw POST bodies at 2am trying to figure out patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting means wiring up KV / Redis.&lt;/strong&gt; Cloudflare KV works but its eventual-consistency model means a bot can hit your form 50 times in the first second before the limit kicks in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email deliverability is a full-time job.&lt;/strong&gt; You need SPF, DKIM, DMARC records, a verified sending domain, and warm-up time. I shipped a form on a Friday and spent the weekend debugging why emails went straight to spam on Gmail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File uploads are painful.&lt;/strong&gt; Multipart parsing in Workers is doable but not pleasant. You'll write a custom parser or pull in a 100KB dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No dashboard for clients.&lt;/strong&gt; If the site owner isn't technical, you'll be exporting CSVs and emailing them every Monday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No analytics.&lt;/strong&gt; Want to know conversion rate from page-view → submission? Build your own analytics pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance is forever.&lt;/strong&gt; Six months in I was still patching edge cases: bot signatures, IP reputation lookups, CSRF tokens, file size limits, MIME type sniffing, GDPR data-export endpoints.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I gave up on this approach after about three months. The function itself was 80 lines. The &lt;em&gt;supporting infrastructure&lt;/em&gt; — spam, email, dashboard, analytics, retention — was easily 2,000 lines and growing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verdict
&lt;/h3&gt;

&lt;p&gt;Best for: engineers with very specific needs who enjoy maintaining infrastructure. Worst for: anyone who wants to spend their time on the actual product, not form plumbing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Approach 4: Pay-per-submission credit packs (FormSubmit)
&lt;/h2&gt;

&lt;p&gt;A middle ground. You host no code. You point a form at FormSubmit and the submission lands in your inbox (and optionally a webhook). No subscription — you buy a one-time credit pack and credits never expire.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"https://formsubmit.co/your@email.com"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FormSubmit's pricing is unusually honest for this category: &lt;strong&gt;100 credits = 100 submissions = $10&lt;/strong&gt;, scaling down to &lt;strong&gt;$0.07/submission at the 2,500-credit pack&lt;/strong&gt;. Credits never expire, no monthly fee, no tier you have to upgrade into.&lt;/p&gt;

&lt;h3&gt;
  
  
  What works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Almost zero setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spam doesn't consume credits&lt;/strong&gt; — only clean submissions are billed against your balance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No subscription trap.&lt;/strong&gt; A weekend project can buy 100 credits once and use them for years.&lt;/li&gt;
&lt;li&gt;Submissions land directly in your inbox, and webhooks are supported out of the box.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What breaks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No persistence.&lt;/strong&gt; Once it's in your inbox, it's gone. No dashboard, no search, no analytics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-submission cost adds up at scale.&lt;/strong&gt; At $0.07/submission, 5,000 clean submissions/month = $350/month — Formspree Pro does the same volume for $20/month. FormSubmit is built for low-volume use, not lead-gen SaaS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lead intelligence.&lt;/strong&gt; No AI classification, no Slack routing by intent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CAPTCHA is per-form opt-in.&lt;/strong&gt; You can disable reCAPTCHA on individual forms (&lt;code&gt;&amp;lt;input type="hidden" name="_captcha" value="false"&amp;gt;&lt;/code&gt;), but doing so hands you back to your own spam problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Verdict
&lt;/h3&gt;

&lt;p&gt;Best for: hobby projects and side sites where you want to pay once and forget about it. Worst for: anything resembling a business with meaningful lead volume.&lt;/p&gt;




&lt;h2&gt;
  
  
  Approach 5: An AI-augmented pipeline (the one I ended up shipping)
&lt;/h2&gt;

&lt;p&gt;After all that, here's the architecture I kept wishing existed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Accept form submission via a simple &lt;code&gt;POST /api/v1/f/{form_id}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run it through an LLM that returns: &lt;code&gt;{ is_spam, spam_confidence, intent, intent_confidence, summary, is_high_value }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If spam → silently drop, don't email, don't count against quota.&lt;/li&gt;
&lt;li&gt;If not spam → email me a digest with the AI summary prepended.&lt;/li&gt;
&lt;li&gt;Store everything in a database with search, filtering, and CSV export.&lt;/li&gt;
&lt;li&gt;Webhook to Slack/CRM with the classification attached.&lt;/li&gt;
&lt;li&gt;Rate-limit at the edge (Redis sliding window).&lt;/li&gt;
&lt;li&gt;Free tier for hobbyists, paid tier for teams.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is essentially what I ended up building. But before you skip to the bottom and click the link — let me show you the AI layer because it's worth understanding even if you don't use my tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  A realistic AI classification prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SPAM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a professional spam-detection specialist.
Analyze the form submission below and decide whether it is spam or legitimate.

Decision criteria:
- Spam ads: product/service promotion, mass-sent links, SEO-bait content
- Malicious messages: phishing links, scams, malware promotion
- Legitimate messages: real customer inquiries, technical support requests, feedback

Only analyze the `message` field.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;INTENT_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a professional form-content analyst.
Identify the user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s true intent.

Intent types:
- sales: pricing, quotes, purchase intent
- support: technical help requests
- general: greetings, generic inquiries
- feedback: comments, suggestions, complaints
- other: anything else

High-value sales signals:
- Clear purchase intent
- Bulk / custom pricing questions
- Specific quantities or budgets
- Enterprise / business inquiry

Generate a concise English summary (under 50 words).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used &lt;code&gt;gpt-4o-mini&lt;/code&gt; for this. Cost per submission: roughly $0.0001. Even at 10,000 submissions/month, that's about a dollar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters in practice
&lt;/h3&gt;

&lt;p&gt;A typical week of form submissions on a B2B SaaS landing page might look like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Raw submission&lt;/th&gt;
&lt;th&gt;Without AI&lt;/th&gt;
&lt;th&gt;With AI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Hi, I'd like a quote for 50 seats of your enterprise plan."&lt;/td&gt;
&lt;td&gt;You read it Monday morning, reply Tuesday.&lt;/td&gt;
&lt;td&gt;Pings your Slack &lt;code&gt;#sales&lt;/code&gt; channel with &lt;code&gt;intent=sales, is_high_value=true&lt;/code&gt;. Rep replies same day.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Buy cheap watches rolex replica &lt;a href="http://www.example.com" rel="noopener noreferrer"&gt;www.example.com&lt;/a&gt;"&lt;/td&gt;
&lt;td&gt;You mark it as spam.&lt;/td&gt;
&lt;td&gt;Auto-classified, never reaches your inbox.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Your docs are great, but the API auth section is confusing."&lt;/td&gt;
&lt;td&gt;You bookmark it, forget to act.&lt;/td&gt;
&lt;td&gt;Pings &lt;code&gt;#docs&lt;/code&gt; channel with &lt;code&gt;intent=feedback&lt;/code&gt;. PM reads it Friday, files an issue.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the difference between a contact form and a &lt;em&gt;lead funnel&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest comparison
&lt;/h2&gt;

&lt;p&gt;Here's the matrix I wish someone had handed me before I spent three weekends on this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Setup time&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;File uploads&lt;/th&gt;
&lt;th&gt;AI features&lt;/th&gt;
&lt;th&gt;Data ownership&lt;/th&gt;
&lt;th&gt;Lock-in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Platform-native (Netlify Forms)&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;td&gt;Unlimited under new credit-based plans (legacy: 100/mo)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Vendor's DB&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SaaS (Formspree / Web3Forms / Basin / Forminit)&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;50–250 subs/mo&lt;/td&gt;
&lt;td&gt;Paid tiers (Forminit includes 100MB on Free)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Vendor's DB&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless function&lt;/td&gt;
&lt;td&gt;1–2 days&lt;/td&gt;
&lt;td&gt;100k req/day (CF)&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;Yours&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form-to-email relay (FormSubmit)&lt;/td&gt;
&lt;td&gt;2 min&lt;/td&gt;
&lt;td&gt;Pay-per-submission credits ($0.07–$0.10/clean submission, never expire)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Your inbox&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI-augmented SaaS&lt;/td&gt;
&lt;td&gt;5 min&lt;/td&gt;
&lt;td&gt;200 subs/mo (free)&lt;/td&gt;
&lt;td&gt;No (use a dedicated service)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yours (CSV export)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few things to read between the lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Setup time&lt;/strong&gt; is misleading for DIY. "1–2 days" is the function. The infrastructure around it (spam, email, dashboard, analytics) is weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data ownership&lt;/strong&gt; matters more than most people think. Vendor shutdowns happen. Price changes happen. If your form is a business-critical lead source, make sure you can export everything as CSV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI features&lt;/strong&gt; aren't a nice-to-have if you get more than ~50 submissions/week. Manual triage stops scaling around that point.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  My final solution (and the honest disclosure)
&lt;/h2&gt;

&lt;p&gt;After going through all four of those approaches for actual client work, I ended up building a tool that combines them: an AI-augmented form backend that you embed with a single &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; tag or a single &lt;code&gt;fetch()&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm the developer behind &lt;a href="https://usesmartform.com" rel="noopener noreferrer"&gt;SmartForm AI&lt;/a&gt;.&lt;/strong&gt; I built it because I kept needing exactly this stack for my own projects and didn't want to wire it up from scratch for the fifth time.&lt;/p&gt;

&lt;p&gt;What it does that the others don't (or don't do well):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI spam filter + intent classification + lead scoring&lt;/strong&gt; out of the box, using a prompt pipeline similar to what I showed above. Legitimate sales inquiries get a Slack ping with &lt;code&gt;is_high_value: true&lt;/code&gt;. Spam never reaches your inbox — and spam doesn't count against your quota, so a bot storm doesn't burn through your free tier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free tier is 200 submissions/month&lt;/strong&gt; with AI spam filtering included, no credit card. SmartForm AI's free tier pairs the AI layer with a generous submission cap — most form-backend free tiers either cap you at 50 submissions (Formspree, Basin) or strip AI out of the free tier entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing stays simple.&lt;/strong&gt; Pro is $9.9/mo for 5,000 submissions; Business is $19.9/mo for 50,000. No "AI filtering $9/mo add-on" bolted on, no tier that excludes webhooks, no surprise overage bills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks fire with the AI classification attached&lt;/strong&gt; so you can route sales inquiries to your CRM, feedback to Linear, and ignore spam. Pro tier adds HMAC signing; Business adds retry + dead-letter queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Formspree-compatible API&lt;/strong&gt; — &lt;code&gt;POST /api/v1/f/{form_id}&lt;/code&gt; with JSON or form-data, so the migration from a Formspree-backed site is a single URL change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few honest caveats so you can decide if it fits: SmartForm AI doesn't currently support file uploads natively (if you need resume or screenshot attachments, you'll still wire up a dedicated upload service like Uploadcare or S3 presigned URLs). The Free tier caps at 200 submissions/month without an automatic upgrade — submissions above that get rejected with HTTP 402 rather than silently upgraded. If either of those is a deal-breaker, Formspree or Forminit will serve you better.&lt;/p&gt;

&lt;p&gt;I won't pretend it's the only tool in this space — Formspree, Web3Forms, Basin, and the rest are solid for the 80% case. SmartForm AI is what I reach for when I want the AI layer and don't want to maintain the infrastructure myself.&lt;/p&gt;

&lt;p&gt;Try it if it fits your use case, ignore it if it doesn't. The matrix above is the only thing I'd actually want you to take away from this post.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;If you're starting from scratch today, my recommendation in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Personal site, &amp;lt; 100 subs/month&lt;/strong&gt; → Web3Forms (250/mo free with custom redirect) or Forminit's free tier (100/mo with 100MB file uploads). Both let you avoid any vendor lock-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Already on Netlify for everything else&lt;/strong&gt; → Netlify Forms. Free and unlimited under the current credit-based plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-volume side project where you'll send &amp;lt; 1,000 forms over its lifetime&lt;/strong&gt; → FormSubmit credit packs. Buy 100 credits once for $10 and forget about subscriptions — they never expire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client marketing site, &amp;lt; 1,000 subs/month&lt;/strong&gt; → Formspree Personal ($10/mo, 200 subs) or Pro ($20/mo, 2,000 subs). Mature, boring, works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business-critical lead source, &amp;gt; 500 subs/month&lt;/strong&gt; → AI-augmented SaaS or a serverless function with an LLM in the loop. The manual-triage tax will eat your week otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance-sensitive (healthcare, finance, EU)&lt;/strong&gt; → Self-hosted serverless function with your own database. Data ownership is non-negotiable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Have I missed an approach? I'd love to hear what you're using — especially if you've found something I haven't tried. Drop it in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this saved you some weekend time, a ❤️ or a follow helps others find it. I'm planning a follow-up on the spam-filtering prompt specifically — let me know in the comments if that's useful.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>jamstack</category>
      <category>webdev</category>
      <category>ai</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
