# Securing your endpoints

> Origin allowlisting for browsers, API-key mode for servers, plus CORS and built-in spam defense.

Lanes Forms secures a submission endpoint on two independent axes: **where the browser request came from** (origin), and **whether a server request carries a key** (submission mode). Understanding the split is the key to choosing the right setup.

## The two axes

- **`allowed_origins`** governs **browser** submissions. A request that carries an `Origin` header must match the allowlist.
- **`submission_auth`** governs **non-browser** submissions (a server, `curl`, or an agent, none of which send an `Origin`). It is either `open` (the default) or `api_key`, and is changed with a `PATCH` on a claimed form.

The important subtlety: **`allowed_origins` applies in both modes.** Switching to `api_key` mode does not loosen or tighten browser rules; it only decides what happens to requests with no `Origin`.

## The full matrix

| Caller | `open` (default) | `api_key` |
| --- | --- | --- |
| Browser, `Origin` in allowlist | Allowed, no credential | Allowed, no credential |
| Browser, `Origin` not in allowlist | `403 origin_not_allowed` | `403 origin_not_allowed` |
| Server / curl / agent (no `Origin`) | Allowed, no credential | Requires `lfk_` key, else `401 api_key_required` |
| localhost origin (dev) | Allowed | Requires `lfk_` key |

An empty `allowed_origins` rejects every browser origin, which is exactly what you want for a form that only your backend submits to.

## Client (browser) interaction

For a public form on a website, the browser is the client and it cannot keep a secret. So the browser is authorized by its **origin**, which the browser sets and page scripts cannot forge.

- Keep the form in **`open` mode**.
- List the sites that may submit in **`allowed_origins`**, for example `["https://example.com"]`.
- Do **not** put any key in the page.

**A key in the browser is a leaked key**

Anything shipped to the browser is readable by anyone. That is why browser forms rely on origin allowlisting instead of secrets. Reserve `api_key` mode for server-to-server use.

## Server-to-server interaction

When only your backend submits, there is no browser and no `Origin` header. Lock the form down:

- Set **`submission_auth` to `api_key`**.
- Send the workspace key on every submit: `Authorization: Bearer lfk_...`.
- Optionally leave `allowed_origins` empty so no browser can submit at all.

See [API keys](/docs/forms/api-keys) for creating and storing the key.

## CORS

CORS is handled per form, not by a global wildcard:

- `OPTIONS /v1/f/{form_id}` preflight echoes the **exact** requesting `Origin` (never `*`), with `Access-Control-Allow-Methods: POST, OPTIONS` and `Allow-Headers: Content-Type, Authorization`. A preflight with no `Origin` returns `400 preflight_origin_required`; a disallowed one returns `403 origin_not_allowed`.
- Every submission response for an allowed origin echoes `Access-Control-Allow-Origin` so page scripts can read success and error bodies. A disallowed origin is never echoed.

## Built-in defenses

Beyond origins and keys, every form gets these for free:

- **Honeypot**: the `_gotcha` field silently absorbs bots. See [Submitting](/docs/forms/submitting#the-honeypot).
- **Rate limiting**: 10 submissions per minute per IP.
- **Size cap**: 64 KB per request.
- **Disposable-email rejection** at provisioning time.
- **Unclaimed cap**: 25 submissions before a claim is required.

IP addresses are stored only as a salted hash, never in the raw.

Next: [Errors](/docs/forms/errors) for the full error vocabulary.
