Documentation

The form snippet

Replace frm_your_form_id with the id we send you. The Turnstile site key below is already correct — it is shared across all Submit Kit forms, and is public by design.

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>

<form action="https://api.submit-kit.com/f/frm_your_form_id" method="post">
  <label>Name  <input name="name" maxlength="100" required></label>
  <label>Email <input name="email" type="email" maxlength="254" required></label>
  <label>Message <textarea name="message" maxlength="5000" required></textarea></label>

  <input name="company_website" type="text" tabindex="-1" autocomplete="off" hidden>

  <div class="cf-turnstile" data-sitekey="0x4AAAAAAEi9UrROlKiU5IMN"></div>
  <button type="submit">Send message</button>
</form>

Leave the company_website input in place and leave it empty. It is a honeypot: it is hidden from people, and bots that fill it in are quietly discarded.

If your site sends a Content-Security-Policy

Four directives matter. Each one, omitted, produces a different and misleading symptom — so it is worth copying the whole block.

script-src  'self' https://challenges.cloudflare.com;
frame-src   https://challenges.cloudflare.com;
form-action 'self' https://api.submit-kit.com;
connect-src 'self' https://api.submit-kit.com;
If you omitWhat you see
script-src / frame-src The verification checkbox never appears at all.
form-action 'self' The email is delivered, but the browser hangs after submitting and never reaches your thank-you page. Browsers check this against the redirect target, not just where the form posts.
connect-src Only affects the JavaScript method. The request is blocked before it leaves the page and the browser reports a generic network error, so it looks like our server is down.

Two ways to handle the response

Two ways to handle the response

Pick one. The first needs no JavaScript at all; the second keeps the visitor on the page. Both are complete — copy either one whole.

1. Redirect to your own thank-you page

The default. On success we redirect the browser to the URL you configured, which must be a page on one of your own registered domains. Nothing else to write, and it works with JavaScript disabled.

Build a real thank-you page rather than sending people back to the form. Redirecting to the same page leaves them staring at their own filled-in fields with no sign anything happened, which reads as failure.

2. Stay on the page with JavaScript

Send Accept: application/json and you get a JSON reply instead of a redirect. This example handles the cases that actually occur, and the turnstile.reset() call is not optional — see below.

<form id="contact-form" action="https://api.submit-kit.com/f/frm_your_form_id" method="post">
  <label>Name  <input name="name" maxlength="100" required></label>
  <label>Email <input name="email" type="email" maxlength="254" required></label>
  <label>Message <textarea name="message" maxlength="5000" required></textarea></label>

  <input name="company_website" type="text" tabindex="-1" autocomplete="off" hidden>
  <div class="cf-turnstile" data-sitekey="0x4AAAAAAEi9UrROlKiU5IMN"></div>

  <button type="submit">Send message</button>
  <p id="form-status" role="status" aria-live="polite"></p>
</form>

<script>
(function () {
  var form   = document.getElementById("contact-form");
  var status = document.getElementById("form-status");
  var button = form.querySelector("button[type=submit]");
  var label  = button.textContent;

  form.addEventListener("submit", function (event) {
    event.preventDefault();
    button.disabled = true;
    button.textContent = "Sending\u2026";
    status.textContent = "";

    fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form)
    })
      .then(function (r) { return r.json(); })
      .then(function (body) {
        if (body.ok) {
          form.innerHTML = "<p role='status'>Thanks \u2014 your message has been sent.</p>";
          return;
        }
        status.textContent = messageFor(body);
        recover();
      })
      .catch(function () {
        status.textContent = "We could not reach the server. Please try again.";
        recover();
      });
  });

  function messageFor(body) {
    if (body.error === "VALIDATION_FAILED" && body.fields && body.fields.length) {
      return "Please check: " + body.fields.map(function (f) { return f.field; }).join(", ");
    }
    return body.message || "Your message could not be sent. Please try again.";
  }

  function recover() {
    button.disabled = false;
    button.textContent = label;
    if (window.turnstile) window.turnstile.reset();   // see below
  }
})();
</script>

Always reset Turnstile after a failure

A Turnstile token is spent the moment we verify it. If a submission fails for any reason and the visitor presses send again, the second attempt carries the same, now-used token and is rejected as a replay — so they see "we could not verify that you are human" no matter how many times they try. Calling turnstile.reset() issues a fresh challenge and makes retrying work.

The same applies without JavaScript: after an error, the visitor must reload the form rather than press the back button, because the cached page still holds the spent token. Our error pages say so.

Why the button changes to "Sending…"

A submission takes around a second: we verify the visitor is human and hand the message to our mail provider before telling you it was sent. A button that looks inert for that second reads as broken, and people click it twice.

Field types

A form may declare up to 20 fields, and the whole request must be under 32 KB. Fields you have not declared are discarded rather than rejected.

Errors

JSON responses carry a stable error code:

Changing your configuration

Recipients, allowed domains, fields, and the success URL are all held in server-side configuration. Email support@submit-kit.com and we will update and redeploy.