Vercel contact form without a backend
Add a working contact form to a site on Vercel without writing a function: one HTML form or fetch() call, email notification, spam filter without captcha.
Vercel serves your site and runs the functions you write, but it does not collect form submissions for you. A <form> on a Vercel site needs an endpoint that receives the POST, stores the message and emails it to you. You can write that endpoint as a Vercel Function, or point the form at a hosted form backend and deploy nothing extra. With Formlane that endpoint is https://smallwerk.com/formlane/api/submit. It works the same for plain HTML, Astro, Hugo, Vite/React, SvelteKit or Next.js on Vercel: no API route, no environment secret, no vercel.json change. Submissions are stored in Frankfurt and emailed to your account address, with reply-to set to the sender.
Steps
- Sign in to Formlane, create a form and copy its access key (
fl_…). - Add the form below to your contact page and replace
fl_YOUR_ACCESS_KEYwith your key. The key is public by design (it only allows sending to this one form), so it is fine in your HTML or client code. - Set the hidden
redirectfield to a thank-you page on your own domain, e.g.https://example.com/thanks. - In the form settings, restrict the form to your production domain, so forms on other websites can't post to it.
- Push to your Git repository (or run
vercel), open the deployed site, send a test message and check your inbox and the Formlane dashboard.
Code
<form action="https://smallwerk.com/formlane/api/submit" method="POST"> <input type="hidden" name="access_key" value="fl_YOUR_ACCESS_KEY"> <input type="hidden" name="subject" value="New message from my Vercel site"> <input type="hidden" name="redirect" value="https://example.com/thanks"> <!-- Honeypot: hidden from people, leave it unchecked --> <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off"> <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> <button type="submit">Send</button> </form>
Works as the contact page of any framework you deploy to Vercel. The email field becomes the reply-to address of the notification, so you can answer with one click.
Optional: stay on the page
Want an inline success message instead of a redirect? Send the same form with fetch(). message in the response is always English, so the snippet shows its own text per error code:
<script>
const MESSAGES = {
limit_reached: "This form can't take more messages this month. Please email us directly.",
rate_limited: "Too many messages in a short time. Please try again in a few minutes.",
origin_not_allowed: "This form only accepts messages from its own website.",
};
const form = document.querySelector('form[action*="formlane"]');
if (form) form.addEventListener("submit", async (event) => {
event.preventDefault();
// FormData skips the unchecked honeypot checkbox, so real visitors never send it.
const data = Object.fromEntries(new FormData(form));
try {
const res = await fetch(form.action, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(data),
});
const json = await res.json();
form.outerHTML = json.success
? "<p>Thanks! Your message has been sent.</p>"
: "<p>" + (MESSAGES[json.error] || "Sorry, that didn't work. Please try again later.") + "</p>";
} catch {
alert("Network error. Please try again.");
}
});
</script>Using React, Vite or Next.js? The React guide has a ready-made component, and the Next.js guide shows the same without an API route.
What the response looks like
With Accept: application/json Formlane answers with JSON:
- Success (HTTP 200):
{"success": true, "message": "Form submitted successfully", "id": "…"} - Error (4xx/5xx):
{"success": false, "message": "…", "error": "…"}. Error codes includeinvalid_key,origin_not_allowed,rate_limitedandlimit_reached.
A plain HTML form (without Accept: application/json) is redirected to your redirect URL after a successful submission.
Limits: 64 KB per submission, 50 fields, 10,000 characters per field, 10 submissions per 10 minutes per visitor and form. File uploads are not supported yet.
Notes for Vercel
- No function in your project: the form posts straight from the visitor's browser to Formlane, so there is no function to deploy, no secret to set and no function usage on your Vercel project for the form.
- Preview deployments: allowed domains match the domain and its subdomains. Vercel preview URLs (for example
my-site-git-feature-my-team.vercel.app) are not subdomains of your production address, so one entry can't cover them. Test on your production domain, or add the exact preview hostname for a moment. Don't addvercel.appon its own: that would accept submissions from every site onvercel.app. - Custom domain: set the
redirectfield to your custom domain, not to the*.vercel.appaddress, so visitors stay on your brand. - Allowed domains are a browser check. They stop forms on other websites from using your key. A script that fakes the
Originheader can still get through; the honeypot, rate limit and content rules still apply to it.
Other ways to do it (honest comparison)
| Option | Good for | Watch out for |
|---|---|---|
| Hosted form backend (Formlane, Formspree, Web3Forms and others) | Contact and lead forms on any site hosted on Vercel; no code to deploy | Monthly submission quotas; each service has its own free tier and feature set (file uploads, webhooks, autoresponders differ) |
| Your own Vercel Function or Next.js Server Action + an email API | Full control, custom logic, data stays in your own stack | You write and maintain validation, spam protection, rate limiting, storage and email delivery yourself |
| Email-from-the-browser services (e.g. EmailJS) | Sending an email straight from client code with a template | Messages are emails, not a stored submission list; check how spam is handled |
| Netlify Forms | Sites deployed on Netlify | Only works for sites hosted on Netlify, not on Vercel |
Formlane is the right pick if you want a plain HTML form on Vercel, submissions stored in Frankfurt and a spam filter without captcha. If you need file uploads, autoresponders, webhooks or a captcha today, choose a service that already has them: Formlane doesn't yet.
Pricing
Free: 250 submissions a month and up to 5 forms. Starter: $8/month for 2,500 submissions. Pro: $19/month for 25,000 submissions. Prices are in USD, plus applicable taxes.
Related guides
FAQ
Does Vercel have built-in form handling?
Vercel hosts your site and runs functions you write, but it does not collect form submissions for you. Either write a function that receives the POST, or point the form's action at a form backend such as Formlane.
Do I need an API route or Vercel Function for a contact form?
Not with a form backend. The form posts directly to https://smallwerk.com/formlane/api/submit, which stores the message and emails it to you. Nothing extra is deployed on Vercel.
Is it safe to put the access key in my HTML?
Yes. The Formlane access key is public by design and only allows sending submissions to that one form. Restrict the form to your domain in its settings, so forms on other websites can't use it.
Does it work with Next.js, Astro, SvelteKit or plain HTML on Vercel?
Yes. The form is plain HTML (or one fetch() call), so it works with any framework Vercel hosts, and on a fully static project.
How is spam filtered?
A hidden honeypot checkbox, a rate limit per visitor and simple content rules (for example link flooding), no captcha. Details in the guide Stop contact form spam without a captcha.
Where are submissions stored?
Submissions are stored in Frankfurt, in a Supabase database (region eu-central-1). The app runs on Vercel and notifications are sent via an email provider; the privacy policy lists all providers.
What happens when I reach the monthly limit?
New submissions are rejected with the error limit_reached until the next calendar month or an upgrade. Stored submissions stay.
Get your access key
Free for 250 submissions a month. Submissions stored in Frankfurt.
Get your access key