Languages and runtimes
A JavaScript contact form without a backend
If you want an inline success message instead of a page navigation, you need one fetch call. That is the entire integration: no SDK, no dependency, no build step. The endpoint accepts both FormData and JSON, and returns JSON whenever you ask for it with an accept header.
Setup
Copy your endpoint
Create a form in the dashboard.
Intercept the submit event
preventDefault, then POST the FormData. Sending the accept: application/json header is what keeps the browser on the page.
Handle the response
A success looks like { ok: true, id: 'sub_…' }. A validation failure returns 422 with an issues array.
JavaScript examples
Swap in your own endpoint and these run as-is.
Progressive enhancement: the form still posts natively if this script fails to load.
const form = document.querySelector("#contact");
const result = document.querySelector("#result");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const button = form.querySelector("button");
button.disabled = true;
try {
const res = await fetch(form.action, {
method: "POST",
headers: { accept: "application/json" },
body: new FormData(form),
});
const data = await res.json();
if (data.ok) {
result.textContent = "Thanks, we got it.";
form.reset();
} else {
result.textContent = data.message ?? "Something went wrong.";
}
} catch {
result.textContent = "Network error. Please try again.";
} finally {
button.disabled = false;
}
});Posting JSON instead, for when the data does not come from a <form> at all.
const res = await fetch("https://send.webforms.to/wft_your_key", {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
},
body: JSON.stringify({
name: "Ada Lovelace",
email: "ada@example.com",
message: "JSON works too.",
_subject: "New enquiry",
}),
});
const data = await res.json();
// { ok: true, id: "sub_…", message: "Thanks! …", tookMs: 42 }Send an Idempotency-Key if you retry, so a flaky connection cannot create duplicates.
const key = crypto.randomUUID();
async function submit(body, attempt = 0) {
const res = await fetch("https://send.webforms.to/wft_your_key", {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
"idempotency-key": key,
},
body: JSON.stringify(body),
});
if (res.status >= 500 && attempt < 3) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
return submit(body, attempt + 1);
}
return res.json();
}Worth knowing
- Reusing an Idempotency-Key across retries returns { ok: true, duplicate: true } instead of storing the submission twice.
- Validation failures come back as 422 with an issues array of { field, message } so you can render errors next to the right input.
JavaScript form FAQ
No. The ingest endpoint allows cross-origin POSTs from any page, because that is how a form on your domain reaches it. Restrict which domains may submit from the form's settings in the dashboard.
Both are accepted. FormData is easier when the data comes from a real form and handles checkbox groups automatically. JSON is cleaner when you have built the object yourself.
Send an Idempotency-Key header and reuse the same value for retries of one logical submission.
Related guides
Where JavaScript submissions can go
The guide above gets the submission to your endpoint. From there it can land anywhere: one destination on Free, several on Pro.
Need the form markup too? The template gallery has complete forms for contact, waitlist, job application and more, and the form generator builds one field by field.
Ready to wire up your JavaScript form?
Free for 300 submissions a month. Endpoint in under a minute, no card required.
Start free