Gatsby Form Backend: Complete Guide to Handling Forms in Gatsby (2026)

Add a form backend to your Gatsby site without GraphQL or server setup. Step-by-step guide with React components, file uploads, validation, and spam protection that blocks 84% of junk.

StaticForm Team

Gatsby Form Backend: Complete Guide to Handling Forms in Gatsby (2026)

Gatsby is brilliant for building fast, SEO-friendly sites. React components, GraphQL data layer, image optimization, prefetching - it handles the hard stuff so you can focus on shipping. But then you need a contact form and everything grinds to a halt.

Gatsby is a static site generator. There’s no server. No API routes. No database. Your beautiful React components render to HTML at build time and get served from a CDN. That’s the whole point - and it’s also the problem when someone needs to submit a form.

We built StaticForm because we got tired of solving this exact problem on every Gatsby project. This guide walks through how to use StaticForm as your Gatsby form backend - from basic HTML forms to React components with hooks, file uploads, and validation.

The Gatsby Form Problem

With Next.js or Remix, you can spin up an API route or server action to handle form submissions. Gatsby doesn’t have that luxury. It generates static HTML. Once it’s built, there’s no server-side code running.

So what do Gatsby developers typically do?

Option 1: Gatsby Functions (Serverless)

Gatsby added serverless functions in v3+. You create a file in src/api/ and it becomes a serverless endpoint. Sounds great, but:

  • Only works on Gatsby Cloud or compatible hosting (not every CDN supports this)
  • You still need to wire up SMTP for email notifications
  • You still need spam protection
  • You still need somewhere to store submissions
  • If you’re on Netlify or Vercel, you’re using their function system, not Gatsby’s

Option 2: Netlify Forms

If you’re hosting on Netlify, their built-in form handling works with Gatsby. Add a data-netlify="true" attribute and you’re done. But:

  • Locks you into Netlify hosting
  • Spam filtering is basic (honeypot + reCAPTCHA)
  • 100 submissions/month on free tier, then it gets expensive fast
  • Every submission counts against your quota, spam included

Option 3: Build Your Own with a Gatsby Function + Email Service

Wire up a serverless function to SendGrid or Resend, add a database, implement spam filtering… and now you’ve spent a weekend building infrastructure for a contact form. On every project.

The Real Problem

Gatsby gives you React in the browser and static HTML on the CDN. What it doesn’t give you is anything that happens after a user clicks “Submit”:

  • Where do submissions go? You need persistent storage.
  • How do you get notified? Email, Slack, something.
  • How do you stop spam? Across our platform, 84% of all form submissions are spam. That’s the reality of putting a form on the internet. For every 10 submissions, roughly 8 are junk.
  • What about GDPR? If you or your users are in the EU, where that data gets stored matters.

This is what a form backend solves. And the nice thing about Gatsby being React under the hood is that any form approach that works with React works with Gatsby - no GraphQL required.

What is StaticForm?

StaticForm is a form backend service. You point your form’s action at a StaticForm endpoint (or POST to it with fetch), and we handle everything else: storing submissions, sending notifications, filtering spam, and giving you a dashboard to manage it all.

The short version:

  • Endpoint-based - your form POSTs to a URL, we take it from there
  • Multi-channel notifications - email, Slack, Discord, webhooks
  • Spam protection - multi-layer detection that catches 84% of spam before it reaches you
  • Spam doesn’t count - unlike competitors, spam submissions don’t eat your quota
  • EU data storage - all data stored in Europe for GDPR compliance
  • Execution logs - see exactly what happened with every notification attempt

No npm packages to install. No GraphQL plugins. No build-time configuration. Just an HTTP endpoint that your Gatsby forms POST to.

Basic HTML Form in Gatsby

The simplest possible approach. This works in any Gatsby page or component and doesn’t require any client-side JavaScript beyond what Gatsby already ships.

// src/pages/contact.js
import React from "react"
import Layout from "../components/layout"
import Seo from "../components/seo"

const ContactPage = () => (
  <Layout>
    <h1>Contact Us</h1>
    <form
      action="https://api.staticform.app/api/v1/forms/YOUR_FORM_ID"
      method="POST"
    >
      <div>
        <label htmlFor="name">Name</label>
        <input type="text" id="name" name="name" required />
      </div>

      <div>
        <label htmlFor="email">Email</label>
        <input type="email" id="email" name="email" required />
      </div>

      <div>
        <label htmlFor="message">Message</label>
        <textarea id="message" name="message" rows={5} required />
      </div>

      {/* Honeypot field - invisible to real users, catches bots */}
      <input type="text" name="_gotcha" style={{ display: "none" }} />

      <button type="submit">Send Message</button>
    </form>
  </Layout>
)

export const Head = () => <Seo title="Contact" />

export default ContactPage

When submitted, the form POSTs to StaticForm, we process it, and redirect the user to your configured success page. You can set the redirect URL in the StaticForm dashboard.

That _gotcha field is a honeypot - it’s hidden from real visitors but bots fill it in automatically. StaticForm uses this as one signal in its spam detection pipeline.

This approach is dead simple and works everywhere Gatsby runs. No hydration issues, no client-side state, no build plugins. Just HTML doing what HTML has done since 1995.

React Component with Hooks

For a better user experience, you probably want to handle submissions with JavaScript. Show a loading spinner, display inline errors, render a success message - all without a full page redirect.

Here’s a reusable contact form component using React hooks:

// src/components/ContactForm.js
import React, { useState } from "react"

const FORM_ENDPOINT = "https://api.staticform.app/api/v1/forms/YOUR_FORM_ID"

const ContactForm = () => {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
  })
  const [status, setStatus] = useState("idle") // idle | submitting | success | error
  const [errorMessage, setErrorMessage] = useState("")

  const handleChange = (e) => {
    const { name, value } = e.target
    setFormData((prev) => ({ ...prev, [name]: value }))
  }

  const handleSubmit = async (e) => {
    e.preventDefault()
    setStatus("submitting")
    setErrorMessage("")

    const data = new FormData()
    Object.entries(formData).forEach(([key, value]) => {
      data.append(key, value)
    })

    try {
      const response = await fetch(FORM_ENDPOINT, {
        method: "POST",
        body: data,
      })

      if (!response.ok) {
        const problem = await response.json()
        throw new Error(problem.detail || "Something went wrong")
      }

      setStatus("success")
      setFormData({ name: "", email: "", message: "" })
    } catch (err) {
      setStatus("error")
      setErrorMessage(err.message || "Failed to send message. Please try again.")
    }
  }

  if (status === "success") {
    return (
      <div className="form-success">
        <h3>Message sent!</h3>
        <p>Thanks for reaching out. We'll get back to you soon.</p>
        <button onClick={() => setStatus("idle")}>Send another message</button>
      </div>
    )
  }

  return (
    <form onSubmit={handleSubmit}>
      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input
          type="text"
          id="name"
          name="name"
          value={formData.name}
          onChange={handleChange}
          required
        />
      </div>

      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          required
        />
      </div>

      <div className="form-group">
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          name="message"
          rows={5}
          value={formData.message}
          onChange={handleChange}
          required
        />
      </div>

      {/* Honeypot - hidden from humans */}
      <input
        type="text"
        name="_gotcha"
        style={{ position: "absolute", left: "-9999px" }}
        tabIndex={-1}
        autoComplete="off"
        aria-hidden="true"
      />

      {status === "error" && (
        <p className="form-error">{errorMessage}</p>
      )}

      <button type="submit" disabled={status === "submitting"}>
        {status === "submitting" ? "Sending..." : "Send Message"}
      </button>
    </form>
  )
}

export default ContactForm

Use it in any Gatsby page:

// src/pages/contact.js
import React from "react"
import Layout from "../components/layout"
import Seo from "../components/seo"
import ContactForm from "../components/ContactForm"

const ContactPage = () => (
  <Layout>
    <h1>Get in Touch</h1>
    <ContactForm />
  </Layout>
)

export const Head = () => <Seo title="Contact" />

export default ContactPage

This component manages its own state with useState - no external state management needed. The form data gets sent as FormData (not JSON), which is what StaticForm expects for standard form submissions.

A few things worth pointing out:

  • No useEffect needed - the form submission is triggered by the submit handler, not a side effect
  • Controlled inputs - React state drives the form values, making it easy to reset after success
  • The honeypot is positioned off-screen with aria-hidden so screen readers skip it too
  • Error handling covers both network failures and API validation errors

TypeScript Version

If you’re using Gatsby with TypeScript (and you should be), here’s the same component typed properly:

// src/components/ContactForm.tsx
import React, { useState, FormEvent, ChangeEvent } from "react"

const FORM_ENDPOINT = "https://api.staticform.app/api/v1/forms/YOUR_FORM_ID"

type FormStatus = "idle" | "submitting" | "success" | "error"

interface FormState {
  name: string
  email: string
  message: string
}

const ContactForm: React.FC = () => {
  const [formData, setFormData] = useState<FormState>({
    name: "",
    email: "",
    message: "",
  })
  const [status, setStatus] = useState<FormStatus>("idle")
  const [errorMessage, setErrorMessage] = useState("")

  const handleChange = (
    e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const { name, value } = e.target
    setFormData((prev) => ({ ...prev, [name]: value }))
  }

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    setStatus("submitting")
    setErrorMessage("")

    const data = new FormData()
    Object.entries(formData).forEach(([key, value]) => {
      data.append(key, value)
    })

    try {
      const response = await fetch(FORM_ENDPOINT, {
        method: "POST",
        body: data,
      })

      if (!response.ok) {
        const problem = await response.json()
        throw new Error(problem.detail || "Something went wrong")
      }

      setStatus("success")
      setFormData({ name: "", email: "", message: "" })
    } catch (err) {
      setStatus("error")
      setErrorMessage(
        err instanceof Error ? err.message : "Failed to send message"
      )
    }
  }

  if (status === "success") {
    return (
      <div role="alert" className="form-success">
        <h3>Message sent!</h3>
        <p>Thanks for reaching out. We'll get back to you soon.</p>
        <button onClick={() => setStatus("idle")}>Send another</button>
      </div>
    )
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* Same JSX as JavaScript version */}
      {/* ... */}
    </form>
  )
}

export default ContactForm

How StaticForm Spam Protection Works

Let’s talk about the elephant in the room. Spam.

If you’ve ever put a form on a website and checked on it a week later, you know the feeling. Your inbox is full of SEO pitches, crypto spam, and gibberish from bots that discovered your endpoint. It’s relentless.

The Numbers

Across all forms on the StaticForm platform, 84% of submissions are spam. That’s not an exaggeration or a scare tactic - it’s what we actually see in production. For every legitimate contact message or lead, there are roughly 5 spam submissions trying to get through.

Here’s why that number matters for your budget: most form backends charge per submission, spam included. If you’re on a plan with 500 submissions/month and 84% are spam, you’re burning through 420 of those on junk. With StaticForm, spam doesn’t count against your quota. You only pay for real submissions.

Multi-Layer Detection

We don’t rely on a single technique. Spam protection on StaticForm works in layers:

  1. Honeypot fields - The _gotcha hidden field catches basic bots that blindly fill every input they find. Simple, zero friction for real users, and surprisingly effective against unsophisticated bots.

  2. IP reputation analysis - We check the submitter’s IP against known spam sources and botnets. Repeat offenders get blocked before their submission is even processed.

  3. Content analysis - We scan submission content for spam patterns: link stuffing, known spam phrases, suspicious formatting, keyword density that screams SEO spam.

  4. Language detection - If your form is for a German-language site and submissions arrive in a language that doesn’t match the expected audience, that’s a signal. Not an automatic block, but it factors into the overall spam score.

  5. Behavioral signals - How quickly was the form filled out? Did the submission come from a headless browser? Was the referrer suspicious? These signals help distinguish real humans from sophisticated bots.

Each submission gets a spam probability score. High-confidence spam gets filtered automatically. Borderline cases are flagged for manual review in your dashboard.

No CAPTCHAs. No “select all the traffic lights.” Your users fill out the form and hit submit. The spam detection happens on our side, invisibly.

File Uploads with Gatsby

Need users to upload files? Resumes on a job application form, images for a support request, documents for a quote - StaticForm handles file uploads out of the box.

// src/components/UploadForm.js
import React, { useState } from "react"

const FORM_ENDPOINT = "https://api.staticform.app/api/v1/forms/YOUR_FORM_ID"

const UploadForm = () => {
  const [status, setStatus] = useState("idle")
  const [fileName, setFileName] = useState("")

  const handleFileChange = (e) => {
    if (e.target.files.length > 0) {
      setFileName(e.target.files[0].name)
    }
  }

  const handleSubmit = async (e) => {
    e.preventDefault()
    setStatus("submitting")

    const formData = new FormData(e.currentTarget)

    try {
      const response = await fetch(FORM_ENDPOINT, {
        method: "POST",
        body: formData,
      })

      if (!response.ok) {
        throw new Error("Upload failed")
      }

      setStatus("success")
    } catch (err) {
      setStatus("error")
    }
  }

  if (status === "success") {
    return (
      <div className="form-success">
        <h3>Application received!</h3>
        <p>We'll review your submission and get back to you.</p>
      </div>
    )
  }

  return (
    <form onSubmit={handleSubmit} encType="multipart/form-data">
      <div className="form-group">
        <label htmlFor="name">Full Name</label>
        <input type="text" id="name" name="name" required />
      </div>

      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input type="email" id="email" name="email" required />
      </div>

      <div className="form-group">
        <label htmlFor="position">Position</label>
        <select id="position" name="position" required>
          <option value="">Select a role</option>
          <option value="frontend">Frontend Developer</option>
          <option value="backend">Backend Developer</option>
          <option value="design">Designer</option>
        </select>
      </div>

      <div className="form-group">
        <label htmlFor="resume">Resume (PDF, max 10MB)</label>
        <input
          type="file"
          id="resume"
          name="resume"
          accept=".pdf,.doc,.docx"
          onChange={handleFileChange}
          required
        />
        {fileName && <span className="file-name">{fileName}</span>}
      </div>

      <div className="form-group">
        <label htmlFor="cover">Cover Letter (optional)</label>
        <textarea id="cover" name="cover" rows={4} />
      </div>

      <input
        type="text"
        name="_gotcha"
        style={{ position: "absolute", left: "-9999px" }}
        tabIndex={-1}
        aria-hidden="true"
      />

      {status === "error" && (
        <p className="form-error">Something went wrong. Please try again.</p>
      )}

      <button type="submit" disabled={status === "submitting"}>
        {status === "submitting" ? "Uploading..." : "Submit Application"}
      </button>
    </form>
  )
}

export default UploadForm

The key here is FormData and encType="multipart/form-data". When you use new FormData(e.currentTarget), it automatically includes file inputs. StaticForm receives the file, stores it securely, and includes a download link in your notification.

A couple of practical notes:

  • File uploads are included with your submission in the dashboard
  • Files are stored in the EU alongside your other submission data
  • The honeypot field works the same way with file upload forms

Form Validation with React Hook Form

For more complex forms, rolling your own validation gets tedious. React Hook Form is a popular choice in the React ecosystem - it’s performant, has a tiny bundle size, and plays nicely with Gatsby.

First, install it:

npm install react-hook-form

Then build a validated form:

// src/components/ValidatedForm.js
import React, { useState } from "react"
import { useForm } from "react-hook-form"

const FORM_ENDPOINT = "https://api.staticform.app/api/v1/forms/YOUR_FORM_ID"

const ValidatedForm = () => {
  const [submitStatus, setSubmitStatus] = useState("idle")
  const [serverError, setServerError] = useState("")

  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
    reset,
  } = useForm()

  const onSubmit = async (data) => {
    setSubmitStatus("submitting")
    setServerError("")

    const formData = new FormData()
    Object.entries(data).forEach(([key, value]) => {
      formData.append(key, value)
    })

    try {
      const response = await fetch(FORM_ENDPOINT, {
        method: "POST",
        body: formData,
      })

      if (!response.ok) {
        const problem = await response.json()
        throw new Error(problem.detail || "Submission failed")
      }

      setSubmitStatus("success")
      reset()
    } catch (err) {
      setSubmitStatus("error")
      setServerError(err.message || "Something went wrong")
    }
  }

  if (submitStatus === "success") {
    return (
      <div role="alert" className="form-success">
        <h3>Thanks for your message!</h3>
        <p>We'll be in touch soon.</p>
        <button onClick={() => setSubmitStatus("idle")}>
          Send another message
        </button>
      </div>
    )
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <div className="form-group">
        <label htmlFor="name">Name *</label>
        <input
          type="text"
          id="name"
          {...register("name", {
            required: "Name is required",
            minLength: {
              value: 2,
              message: "Name must be at least 2 characters",
            },
          })}
          aria-invalid={errors.name ? "true" : "false"}
        />
        {errors.name && (
          <p className="field-error" role="alert">
            {errors.name.message}
          </p>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="email">Email *</label>
        <input
          type="email"
          id="email"
          {...register("email", {
            required: "Email is required",
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: "Please enter a valid email address",
            },
          })}
          aria-invalid={errors.email ? "true" : "false"}
        />
        {errors.email && (
          <p className="field-error" role="alert">
            {errors.email.message}
          </p>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="subject">Subject</label>
        <select id="subject" {...register("subject")}>
          <option value="general">General Inquiry</option>
          <option value="project">Project Discussion</option>
          <option value="support">Support</option>
          <option value="other">Other</option>
        </select>
      </div>

      <div className="form-group">
        <label htmlFor="message">Message *</label>
        <textarea
          id="message"
          rows={6}
          {...register("message", {
            required: "Message is required",
            minLength: {
              value: 10,
              message: "Please write at least 10 characters",
            },
          })}
          aria-invalid={errors.message ? "true" : "false"}
        />
        {errors.message && (
          <p className="field-error" role="alert">
            {errors.message.message}
          </p>
        )}
      </div>

      {/* Honeypot */}
      <div style={{ position: "absolute", left: "-9999px" }} aria-hidden="true">
        <input type="text" {...register("_gotcha")} tabIndex={-1} />
      </div>

      {serverError && (
        <div role="alert" className="form-error">
          {serverError}
        </div>
      )}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "Sending..." : "Send Message"}
      </button>
    </form>
  )
}

export default ValidatedForm

React Hook Form uses uncontrolled components under the hood (refs instead of state), which means fewer re-renders. On a Gatsby site with lots of components, that performance difference can be noticeable - especially on lower-powered devices.

The register function connects each input to the form’s validation system. When validation fails, errors appear instantly without a round trip to the server. When everything passes, the data gets sent to StaticForm as FormData.

Production Tips for Gatsby Deployments

Environment Variables

Don’t hardcode your form endpoint in components. Use Gatsby’s built-in environment variable support:

# .env.production
GATSBY_STATICFORM_ENDPOINT=https://api.staticform.app/api/v1/forms/YOUR_FORM_ID
// In your component
const FORM_ENDPOINT = process.env.GATSBY_STATICFORM_ENDPOINT

The GATSBY_ prefix is important - without it, the variable won’t be available in client-side code. This is a Gatsby-specific requirement for embedding environment variables at build time.

Works on Every Hosting Platform

Since StaticForm is just an HTTP endpoint, your Gatsby form works identically on:

  • Gatsby Cloud - no special configuration needed
  • Netlify - works without Netlify Forms, no vendor lock-in
  • Vercel - no serverless functions required
  • AWS Amplify - standard static hosting
  • GitHub Pages - yes, even there
  • Any CDN - it’s just HTML and JavaScript posting to a URL

This is one of the underrated benefits of using an external form backend with Gatsby. Your deployment target is irrelevant. Move from Netlify to Vercel? Your forms keep working. No configuration changes, no migration.

Redirect Configuration

If you’re using the basic HTML form (no JavaScript submission), configure your success and error redirect URLs in the StaticForm dashboard. Set them to pages on your Gatsby site:

// src/pages/thank-you.js
import React from "react"
import Layout from "../components/layout"

const ThankYouPage = () => (
  <Layout>
    <h1>Thanks for your message!</h1>
    <p>We'll get back to you within 24 hours.</p>
  </Layout>
)

export default ThankYouPage

Then set https://yoursite.com/thank-you/ as the success redirect in the dashboard. (Note the trailing slash - Gatsby generates directory-style URLs by default.)

Styling with CSS Modules or Styled Components

Gatsby supports CSS Modules out of the box. Here’s how to style your form component:

/* src/components/ContactForm.module.css */
.form {
  max-width: 600px;
  margin: 0 auto;
}

.formGroup {
  margin-bottom: 1.5rem;
}

.formGroup label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 600;
}

.formGroup input,
.formGroup textarea,
.formGroup select {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 6px;
  font-size: 1rem;
}

.formGroup input:focus,
.formGroup textarea:focus {
  outline: none;
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.submitButton {
  background: #3b82f6;
  color: white;
  padding: 0.75rem 2rem;
  border: none;
  border-radius: 6px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
}

.submitButton:hover {
  background: #2563eb;
}

.submitButton:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.error {
  color: #dc2626;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

.success {
  background: #f0fdf4;
  border: 1px solid #bbf7d0;
  padding: 1.5rem;
  border-radius: 8px;
  text-align: center;
}

Import it in your component:

import * as styles from "./ContactForm.module.css"

// Then use: className={styles.formGroup}, className={styles.submitButton}, etc.

Gatsby Head API for SEO

Don’t forget to add proper meta tags to your contact page. Gatsby’s Head API (v4.19+) makes this straightforward:

export const Head = () => (
  <>
    <title>Contact Us | Your Site</title>
    <meta
      name="description"
      content="Get in touch with us. We typically respond within 24 hours."
    />
  </>
)

StaticForm vs Building Your Own

Here’s the honest math for a Gatsby project:

Building your own form handling:

  • Gatsby Function or external serverless function: 1-2 hours
  • SMTP setup (Resend, SendGrid, Postmark): 1-2 hours
  • Spam filtering implementation: 2-4 hours
  • Storage (database or file-based): 1-2 hours
  • Dashboard to view submissions: 4-8 hours
  • Ongoing maintenance per year: 5-10 hours
  • Total: 14-28 hours per project

Using StaticForm:

  • Create form, copy endpoint URL: 5 minutes
  • Set up notifications: 5 minutes
  • Total: 10 minutes

You get spam protection, notification delivery with retry logic, execution logs, GDPR-compliant storage in Europe, and a dashboard to review everything. No SMTP keys to rotate. No database migrations. No serverless function cold starts.

The tradeoff: you’re adding a dependency on an external service. If StaticForm goes down, your form submissions won’t process until it’s back up. That’s a real consideration. We maintain high uptime and store every submission regardless of notification delivery status, but it’s something you should be aware of.

Getting Started

Here’s the fastest path from zero to a working Gatsby form:

  1. Create a free account at app.staticform.app
  2. Create a form and copy your endpoint URL
  3. Pick a component from the examples above (basic HTML, React hooks, or React Hook Form)
  4. Replace YOUR_FORM_ID with your actual endpoint
  5. Submit a test - every form gets 10 free test credits, no credit card required
  6. Set up notifications in the dashboard - email, Slack, Discord, or webhooks

That’s it. Your Gatsby site has a working form with spam protection, submission storage, and notification delivery. No GraphQL queries. No serverless functions. No build plugins.

If you outgrow the free credits, plans start at €4/month for 500 submissions. And since spam doesn’t count against your quota, you’re only paying for real submissions.

Try StaticForm free - 10 free credits per form, no credit card required.