# HTML forms

> Post any HTML form to Ikiform with no JavaScript: redirects, reserved fields, arrays and the honeypot.

A plain HTML form is the whole integration. No script, no iframe and no embed: set the `action` to your form's endpoint and keep your own markup and CSS. This also works in site builders that let you paste custom HTML; see [Site builders](https://ikiform.com/docs/guides/site-builders.md).

## A basic form

`contact.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label>
    Name
    <input name="name" autocomplete="name" required />
  </label>
  <label>
    Email
    <input name="email" type="email" autocomplete="email" required />
  </label>
  <label>
    Message
    <textarea name="message" rows="5"></textarea>
  </label>
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send</button>
</form>
```

- `method` must be `post`. The browser sends `application/x-www-form-urlencoded`, or `multipart/form-data` if you set `enctype`. Both work.
- Every control with a `name` becomes a field. Controls without a name, and disabled controls, are not sent by the browser.
- Values are stored as UTF-8 text, normalized to NFC, with CRLF line endings turned into LF and control characters removed.
- Browser validation such as `required` and `type="email"` helps people, but it is not a security check. Define [fields](https://ikiform.com/docs/fields.md) to have Ikiform enforce rules on the server.

## After submit

A successful browser post is answered with a `303` redirect. Ikiform picks the target in this order:

1. The form's **Redirect after submit** setting in the dashboard. Any `http:` or `https:` URL works here, because only workspace members can set it.
2. A `_redirect` (or `_next`) field in the post, if it passes the rules below.
3. Otherwise Ikiform's hosted thank-you page at `/f/<formId>/thanks`. When the form has allowed domains and the post came from an `https:` page on one of them, the page links back to it.

### Rules for _redirect

A `_redirect` value comes from the visitor's browser, so Ikiform only follows it when it is an `https:` URL with no username, password or custom port, on one of the form's allowed domains or a subdomain of one. Forms without allowed domains ignore `_redirect` entirely. With allowed domains set to `example.com`:

| _redirect | Followed | Why |
| --- | --- | --- |
| `https://example.com/thanks` | Yes | Allowed domain |
| `https://app.example.com/done` | Yes | Subdomain of an allowed domain |
| `http://example.com/thanks` | No | Not `https:` |
| `https://example.com:8443/thanks` | No | Custom port |
| `https://user@example.com/` | No | Contains a username |
| `https://example.com.evil.dev/` | No | Not the domain or a subdomain of it |

A `_redirect` that isn't followed falls through to the hosted thank-you page; it never causes an error. JSON requests ignore redirects and get `201` with the submission id.

### When a post is rejected

If a post is rejected, for example because a required field is missing, the browser sees a hosted error page with the HTTP status of the error. It lists the problems by field label and links back to the form. See [Errors](https://ikiform.com/docs/errors.md) for every code.

## Field names and arrays

Repeated names and names ending in `[]` become arrays. Use `[]` for checkbox groups and `<select multiple>` so that a single choice is still stored as an array.

`fields.html`:

```html
<fieldset>
  <legend>Topics</legend>
  <label><input type="checkbox" name="topics[]" value="billing" /> Billing</label>
  <label><input type="checkbox" name="topics[]" value="api" /> API</label>
</fieldset>

<select name="plan">
  <option value="starter">Starter</option>
  <option value="team">Team</option>
</select>

<label><input type="checkbox" name="newsletter" /> Send me product news</label>
```

Checking both topics and the newsletter box stores:

`Stored fields`:

```json
{
  "topics": ["billing", "api"],
  "plan": "team",
  "newsletter": "on"
}
```

- Names in brackets such as `address[city]` are kept literally; they are not turned into nested objects. Post JSON if you want nested values.
- An unchecked checkbox is not sent at all. With a `checkbox` [field](https://ikiform.com/docs/fields.md#types) defined, a missing box is stored as `false` and a checked one as `true`.
- A submission can have up to 100 fields, names up to 128 characters and values up to 16 KB. See [Limits](https://ikiform.com/docs/limits.md).

> File uploads are not built yet. An empty file input is ignored; a post that contains a real file is refused with `422 files_not_supported`.

## Reserved fields

Names that start with `_` are instructions for Ikiform. They are never stored as answers.

| Field | Effect |
| --- | --- |
| `_gotcha` | Honeypot. Leave it empty and hidden; a filled-in value adds 5 to the spam score, which is enough to store the post as spam. The name can be changed per form. |
| `_redirect`, `_next` | Where to send the browser after a successful post. See [Rules for _redirect](#redirect-rules). |
| `_subject` | Subject for notification emails. Whitespace is collapsed and it is cut to 200 characters. |
| `_replyto` | Reply-to address for notification emails. Ignored unless it is one valid email address. |
| `_idempotency` | Idempotency key, the same as the `Idempotency-Key` header. See [Idempotency](https://ikiform.com/docs/frameworks.md#idempotency). |
| `_ikiform_ts` | Signed render time, set by the SDK and the shadcn/ui component. A post sent under 2 seconds after render scores as spam. |
| `cf-turnstile-response` | The Turnstile token, added by the Turnstile widget or the SDK. |
| `_ikiform_via` | Set by Ikiform's own human check page. Don't send it yourself. |

Any other field starting with `_` is also dropped, so you can use names like `_source` for your own hidden markers without storing them.

`demo.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <input name="email" type="email" required />
  <input type="hidden" name="_subject" value="New demo request" />
  <input type="hidden" name="_redirect" value="https://example.com/thanks" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Request a demo</button>
</form>
```

## The honeypot

The default honeypot name is `_gotcha`. You can change it per form under **Honeypot field** in the form's settings; whatever name you pick is treated as reserved and never stored. A plain `hidden` attribute is enough for most bots. If you see spam getting through, hide the input off-screen instead, because some bots skip inputs marked `hidden`:

`honeypot.html`:

```html
<div aria-hidden="true" style="position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden">
  <label>
    Leave this empty
    <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </label>
</div>
```

Keep `tabindex="-1"` and `autocomplete="off"` so keyboard users and password managers never fill it in.

## Adding a human check

Forms with the spam check set to **Ikiform** need no change in your HTML. When a browser posts without a token, Ikiform answers with a short "Checking you're human" page on ikiform.com. It runs Cloudflare Turnstile, then posts the original fields again with the token. Your visitor ends on the same thank-you page as before. See [Spam protection](https://ikiform.com/docs/spam.md#human-check) for the full flow.

### With your own Turnstile keys

If the form uses **Your own Turnstile keys**, render the widget yourself. It adds the `cf-turnstile-response` field to the post. Posts without a valid token are rejected with `403 captcha_failed`.

`turnstile.html`:

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

<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <input name="email" type="email" required />
  <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
  <button>Send</button>
</form>
```
