send.webforms.toRead the docs →

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.

Get an endpointRead the docs

Setup

  1. Copy your endpoint

    Create a form in the dashboard.

  2. Intercept the submit event

    preventDefault, then POST the FormData. Sending the accept: application/json header is what keeps the browser on the page.

  3. 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.

contact.js
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.

submit.js
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.

retry.js
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

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.

Ready to wire up your JavaScript form?

Free for 300 submissions a month. Endpoint in under a minute, no card required.

Start free