# Submit Kit — integration guide

You have been onboarded and have a form ID that looks like
`frm_A7bGe1aMxDuoX6oxJ2Kbh6`. This file is everything needed to go live.
Wherever you see `frm_YOUR_FORM_ID` below, use yours.

Questions or changes (new field, another domain, different recipient):
**support@submit-kit.com** — we make the change and confirm.

---

## 1. The form

Paste this into your page and adjust the fields to the ones configured for you:

```html
<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>

  <!-- Honeypot: leave it in, leave it empty, keep it hidden. Bots that fill
       it in are silently discarded. Do not remove it. -->
  <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>
```

That's a complete integration. On success, the visitor is redirected to the
thank-you page configured for your form — build a real one; sending people back
to the form they just filled in reads as failure.

Only fields configured for your form are accepted. Anything else in the request
is discarded, and the recipient, sender, subject and redirect cannot be changed
by the request — they live in server-side configuration.

## 2. If your site sends a Content-Security-Policy

Copy the whole block. Each directive omitted fails differently, and none of the
symptoms mentions CSP:

```
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 omit | What you see |
|---|---|
| `script-src` / `frame-src` | The verification checkbox never appears |
| `form-action 'self'` | The email is delivered, but the browser hangs after submitting — browsers check this against the redirect target, not just where the form posts |
| `connect-src` | JavaScript method only: the request is blocked before it leaves the page and looks like our server is down |

## 3. Optional: stay on the page with JavaScript

Send `Accept: application/json` and you get JSON instead of a redirect. This
example is complete — the `turnstile.reset()` call is **not** optional: a
Turnstile token is spent the moment it is verified, so without it every retry
after a failure replays a dead token and fails with "we could not verify that
you are human".

```html
<form id="contact-form" action="https://api.submit-kit.com/f/frm_YOUR_FORM_ID" method="post">
  <!-- same fields, honeypot and cf-turnstile div as above -->
  <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…";
    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 — 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();  // essential — see above
  }
})();
</script>
```

## 4. Field types and limits

| Type | Limit |
|---|---|
| `text` | single line, up to 500 characters |
| `email` | validated address, up to 254 characters |
| `textarea` | multi-line, up to 5,000 characters |
| whole request | 32 KB |

## 5. Error codes (JSON mode)

| Code | Status | Meaning |
|---|---|---|
| `VALIDATION_FAILED` | 400 | `fields` lists names and reasons — never the values typed |
| `BOT_REJECTED` | 403 | Turnstile failed, or a token was reused — reset and retry |
| `ORIGIN_REJECTED` | 403 | The posting site is not on your form's domain list |
| `RATE_LIMITED` | 429 | Retry after the `Retry-After` header |
| `QUOTA_EXCEEDED` | 402 | The form's monthly allowance is spent. Not a retry — top up, or contact us |
| `DELIVERY_FAILED` | 502 | We could not hand the message to our mail provider |
| `TURNSTILE_UNAVAILABLE`, `MAINTENANCE` | 503 | Temporary — worth retrying |

## 6. Worth knowing

- **Replies work naturally.** The visitor's address becomes `Reply-To`, so you
  answer enquiries from your own mail client.
- **We do not intentionally store submission content after delivery.** There is
  no archive and no way to resend a lost message — the email in your inbox is
  the record. The claim is deliberately qualified rather than absolute: your
  message still crosses Cloudflare, AWS and your own mail provider, each of
  which keeps its own records, and we will not promise otherwise.
- **Test with a real submission** once the form is on your page, and confirm
  that hitting reply addresses the visitor.
