Beginner9 minUpdated Aug 14, 2026

How to Add a Working Contact Form to a Claude-Built Site

Ask Claude for a landing page and you get one, contact form included — but that form has no backend, so submissions go nowhere. Here is the fix, including the version you can paste straight into your prompt.

TL;DR

A site or artifact Claude builds is frontend code, so its contact form has nowhere to deliver messages. Give the form a real endpoint: action="https://formslist.com/f/YOUR_HASH" method="POST" for plain HTML, or fetch() with Accept: application/json in a React artifact. You can provision an endpoint with one curl call and no signup, then tell Claude to use it. Free plan: 5 forms, 500 submissions/month, email notifications included.

By Vaibhav Jain·Published August 14, 2026

Prerequisites

  • A page, artifact, or site generated by Claude
  • The ability to edit the code or ask for a revision
  • An email address where submissions should be delivered
1

Why a Claude-generated contact form doesn't deliver anything

Claude writes excellent frontend code. Ask for a landing page with a contact section and you get semantic markup, sensible validation attributes, a loading state on the button, and a success message — a form that is, as a piece of frontend engineering, complete. It still cannot deliver a message, because delivery is not a frontend problem. Three things have to exist outside the browser: a server listening at a public URL to receive the POST, durable storage so the payload is not lost the instant the request ends, and an authenticated mail relay — an account with a mail provider, a verified sending domain, DNS records proving you may send as that domain. None of it can be generated into a file, and none of it can be embedded in browser code, because credentials in browser code are public. The result is a form that reports success without sending. Sometimes the handler only calls setSubmitted(true). Sometimes it posts to a relative path like /api/contact that does not exist wherever the page ends up hosted, and the fetch fails into a catch block that shows a generic message or nothing at all. If you then publish the page to static hosting — GitHub Pages, Netlify, Vercel, an S3 bucket — there is definitively no server to answer, so a relative API path returns your own HTML instead of a response.

// The two shapes this usually takes:

// 1. Success is a state flag. Nothing left the browser.
const handleSubmit = (e) => { e.preventDefault(); setSubmitted(true); };

// 2. A POST to a route that doesn't exist on static hosting.
await fetch("/api/contact", { method: "POST", body: JSON.stringify(data) });
// -> 404, or your index.html returned as the "response"
2

Get a real endpoint in one command, no signup

The missing piece is a URL that is always up, accepts POSTs, stores every submission, filters spam, and emails you. FormsList gives you one, and you do not need an account to start. One POST to the instant provisioning endpoint — supplying the email that should receive submissions and a name for the form — returns a live endpoint immediately. No signup, no OAuth, no credit card. This exists precisely because AI assistants build finished sites in a single conversation, before anyone has signed up for anything; the endpoint has to be obtainable at generation time or the site ships broken. The response includes the endpoint URL, a paste-ready HTML snippet, and a claim link, and the email address you gave receives a claim message so the site owner can take over the dashboard and read past submissions. Unclaimed forms are deleted after 7 days, so claim it once the site is real. Signing up normally at formslist.com and creating the form from the dashboard is equally valid if you already know this is going into production.

curl -X POST https://formslist.com/api/v1/instant \
  -H 'Content-Type: application/json' \
  -d '{"email":"you@example.com","form_name":"Contact"}'

# Response:
#   endpoint      -> https://formslist.com/f/YOUR_HASH
#   html_snippet  -> paste-ready markup
#   claim_url     -> claim the dashboard (also emailed)
#   expires_at    -> unclaimed forms are deleted after 7 days
3

Tell Claude to use it

The most efficient move is to give Claude the endpoint up front, so the very first version of the page has a working form instead of a placeholder. Claude will not invent a form backend on its own, and it should not — but given a concrete endpoint it will wire it in correctly. Be explicit about which style you want. "Use a plain HTML form that posts to this URL" produces a native form with an action attribute. "Use fetch with an Accept: application/json header so the page does not navigate" produces the in-page version with your own success state. Ask for the honeypot by name too, otherwise you will be adding it yourself later. If the page is already built, the retrofit is a one-line request: replace the submit handler with a POST to this endpoint. Because the endpoint accepts standard form encodings, no other part of the page needs to change — the markup, styling, and field names all stay exactly as they are.

Add a working contact form to this page.

Post submissions to: https://formslist.com/f/YOUR_HASH

Requirements:
- Fields: name, email, message (all required)
- Use fetch() with method POST and header "Accept: application/json"
  so the page does not navigate away
- Send the body as FormData built from the form element
- Show a success state when the response is ok, an error message otherwise
- Include a hidden honeypot input named "_gotcha" (display:none, tabindex -1)
4

The React version (for artifacts and React pages)

Artifacts and most Claude-generated app code are React, so the working version is a fetch() call in onSubmit. Keep whatever markup and styling you already have; you are only replacing the body of the handler. The Accept: application/json header is what keeps you on the page. FormsList selects its response format from that header: with it you get a JSON body containing ok: true and a submission id, so your component can render its own success state. Without it, the endpoint treats the request as a browser form post and answers with a 303 redirect to a hosted thank-you page — correct for plain HTML, disruptive inside a single-page React view. Use new FormData(form) rather than assembling an object by hand. Every input with a name is included automatically, so adding a phone or budget field is a markup change and nothing more, and file inputs start working with no handler changes. One artifact-specific note: an artifact preview runs inside a sandboxed frame on a Claude-owned domain, and sandboxes can restrict what network requests are permitted. If a submission does not go through inside the preview but the code looks right, test the same code on real hosting before concluding it is broken — the deployed copy is what your visitors will use anyway.

import { useState } from "react";

const FORM_ENDPOINT = "https://formslist.com/f/YOUR_HASH";

export default function ContactForm() {
  const [status, setStatus] = useState("idle"); // idle | sending | sent | error

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus("sending");

    const form = e.currentTarget;

    try {
      const res = await fetch(FORM_ENDPOINT, {
        method: "POST",
        headers: { Accept: "application/json" }, // JSON response, no navigation
        body: new FormData(form),
      });

      if (!res.ok) throw new Error("Request failed");
      setStatus("sent");
      form.reset();
    } catch {
      setStatus("error");
    }
  }

  if (status === "sent") {
    return <p className="text-emerald-600">Thanks — your message came through.</p>;
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <input name="name" type="text" placeholder="Your name" required />
      <input name="email" type="email" placeholder="you@example.com" required />
      <textarea name="message" rows={5} placeholder="Your message" required />
      {/* Honeypot: bots fill it, humans never see it */}
      <input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: "none" }} />
      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending..." : "Send message"}
      </button>
      {status === "error" && <p className="text-red-600">Something went wrong — please try again.</p>}
    </form>
  );
}
5

The plain HTML version (for a page you publish anywhere)

If Claude produced a single HTML file — the usual output when you ask for a landing page you can host yourself — you want the native form. It needs no JavaScript at all, which means nothing can silently fail between the click and the request. The browser navigates on submit and FormsList replies with a 303 redirect to a hosted thank-you page. To send visitors somewhere of your own instead, add a hidden input named _next containing that URL. This version works identically on GitHub Pages, Netlify, Vercel, Cloudflare Pages, an S3 bucket, or a folder on any web server, because it makes no assumptions about the host. That is precisely the property you want in a page whose hosting you have not decided on yet — and it is the same pattern described at formslist.com/static-site-forms.

<form action="https://formslist.com/f/YOUR_HASH" method="POST">
  <label for="name">Name</label>
  <input id="name" type="text" name="name" required />

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

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>

  <!-- Honeypot: keep hidden, keep empty -->
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none" />

  <!-- Optional: your own thank-you page -->
  <input type="hidden" name="_next" value="https://your-site.example.com/thanks" />

  <button type="submit">Send message</button>
</form>
6

Spam protection, notifications, and file uploads

Spam protection: that hidden _gotcha input is a honeypot. Automated submitters fill in every field they can find, so a submission that arrives with it populated is flagged, while real visitors never see it because it is display:none. No third-party script, no checkbox, no image puzzle — legitimate visitors pay nothing. Automatic filtering runs on top of it on every plan including free, and endpoints also accept reCAPTCHA, Turnstile, and hCaptcha tokens if you want a visible challenge. Email notifications: included on every plan, free included. There is no tier where messages sit in a dashboard until you upgrade — the point of a contact form is that the message reaches a human. Submissions are stored as well as emailed, so nothing is lost if a notification is filtered. File uploads: add enctype="multipart/form-data" to a plain HTML form and include a file input; a fetch call using FormData sets the encoding for you. The free plan accepts 1 file up to 2MB per submission and paid plans accept up to 5 files at 4MB each — enough for a screenshot or a brief without your contact form becoming file storage.

<!-- Uploads: multipart encoding + a file input -->
<form action="https://formslist.com/f/YOUR_HASH" method="POST" enctype="multipart/form-data">
  <input type="text" name="name" required />
  <input type="email" name="email" required />
  <textarea name="message" required></textarea>
  <input type="file" name="attachment" />
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none" />
  <button type="submit">Send</button>
</form>
7

Verify delivery on the published page

Test from wherever the page actually lives, and send something you will recognise. Then check two places: the FormsList dashboard and your inbox. A submission visible in the dashboard but missing from email means the notification address is wrong or the mail was filtered — check spam before assuming the form is broken. If nothing arrives, read the POST response in the browser network tab. A 404 means the hash in the endpoint URL is wrong or the form no longer exists, which is worth remembering if you provisioned instantly and let the 7-day claim window pass. A 422 means server-side validation rejected a field. Error responses include an error code plus a fix field naming the corrective action, so the response body usually tells you what to change without guesswork. A GET request to your endpoint returns JSON usage documentation for that exact form — accepted content types, expected fields, an example request. Useful for you, and useful to paste back into a conversation when you want Claude to correct its own wiring. Broader reference: formslist.com/docs, the programmatic API at formslist.com/form-api, and formslist.com/ai for the agent-facing provisioning flow.

# What does this endpoint accept?
curl https://formslist.com/f/YOUR_HASH

# Send a test submission
curl -X POST https://formslist.com/f/YOUR_HASH \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"name":"Test","email":"test@example.com","message":"Verifying delivery"}'

Frequently Asked Questions

Ready to collect form submissions?

Set up your form backend in under a minute. No server required, no complex configuration — just a simple endpoint for your forms.