Submitting with JavaScript keeps visitors on the page and lets you show inline feedback. Send the request with Accept: application/json and FormSubmit replies with JSON instead of a redirect.
With FormData (recommended)
FormData works for every field type, including files, and doesn't need a Content-Type header — the browser sets the multipart boundary.
const form = document.querySelector("#contact");
const renderedAt = Date.now();
form.addEventListener("submit", async (event) => {
event.preventDefault();
const data = new FormData(form);
data.set("_ts", String(renderedAt)); // time-to-submit spam check
const res = await fetch(form.action, {
method: "POST",
body: data,
headers: { Accept: "application/json" },
});
const json = await res.json();
if (json.ok) {
form.reset();
form.querySelector(".status").textContent = json.message;
} else {
form.querySelector(".status").textContent = json.error.message;
}
});Keep action and method="POST" on the form: if JavaScript fails to load, the form still works as a normal HTML post.
With JSON
await fetch("https://formsubmit.app/f/YOUR_FORM_ID", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ email: "ada@example.com", message: "Hello", _subject: "Website message" }),
});JSON bodies can't contain files; use FormData or direct uploads for those.
Handling errors
Always check json.ok. The error.code is stable and safe to branch on; error.message is human-readable. See the full list in the endpoint reference.
if (!json.ok && json.error.code === "rate_limited") {
showMessage("You're sending messages too quickly — please wait a minute.");
}Frameworks
The form generator exports ready-made components for React, Next.js, Vue, Nuxt, Svelte and Astro that follow this pattern, including loading states and the honeypot field. Framework walkthroughs live in the guides.