# How it works

> Every check a submission goes through, in order, and what each one can return.

Each post to `/f/<formId>` runs through the same pipeline. Cheap checks run first, so a flood or an unknown form costs almost nothing. The first check that fails decides the response.

The pipeline, in order:

1. IP flood guard: at most 600 posts a minute from one IP across every form, before the form is looked up. Fails with `429 rate_limited`.
2. Form lookup: unknown, deleted and disabled forms fail with `404 not_found`.
3. Form rate limits: the form's preset, counted per visitor IP and per form. Fails with `429 rate_limited`.
4. Paused forms fail with `403 form_paused`.
5. Body parsing: urlencoded, multipart or JSON, at most 256 KB. Fails with `415`, `400 invalid_body`, `413 payload_too_large`, `422 too_deep` or `422 files_not_supported`.
6. API key, when an `Authorization` header is sent. Fails with `403 forbidden` or `403 origin_not_allowed`.
7. Allowed domains: a post from another site is stored as spam, or fails with `403 origin_not_allowed`.
8. Human check: fails with `403 challenge_required` or `403 captcha_failed`. Browser form posts get a check page instead.
9. Field validation: fails with `422 validation_failed`.
10. Spam scoring: a score of 5 or more is stored as spam but still answered as a success.
11. Idempotency: a repeated key replays the first response, or fails with `409` or `422`.
12. Monthly usage: far over the limit fails with `429 quota_exceeded`.
13. Store: `201` with the submission id, or a `303` redirect for browser posts. Webhooks, notification emails and the live dashboard update follow.

## What each stage does

### Rate limits before anything else

Rate limits run before the body is read. The flood guard (600 a minute per IP) runs even before the form lookup, so a flood never reaches the database. The form's own rules run right after the lookup. Rejected requests don't use up the allowance. Exact numbers are on [Rate limits](https://ikiform.com/docs/rate-limits.md).

### Parsing

The body is parsed as urlencoded, multipart or JSON. Text is decoded as UTF-8, normalized to NFC, CRLF becomes LF and control characters are removed. Names that start with `_` are reserved instructions, such as `_redirect` and `_subject`, and are never stored as answers. See [HTML forms](https://ikiform.com/docs/html.md#reserved-fields).

### Who sent it

An `Authorization: Bearer` header is optional. A secret key (`ik_sk_`) with the `submissions:write` scope skips the allowed-domains check and the Ikiform human check. A publishable key (`ik_pub_`) only works from the domains listed on the key. Without a key, the post is judged by its `Origin`.

### Spam

Spam is never an error. A post that fails the allowed-domains policy in spam mode, or scores 5 or more, is stored with status `spam` and gets the same success response as a real one, so bots learn nothing. Spam sends no webhooks or emails and doesn't count toward usage. See [Spam protection](https://ikiform.com/docs/spam.md).

### Store and deliver

The submission, its webhook deliveries and its notification email are written in one database batch, then handed to a queue. Webhook delivery and email never slow down the response, and a submission is never stored without its deliveries.

## JSON or a redirect

The same endpoint answers browsers with pages and code with JSON. Ikiform decides per request:

| Request | Answer |
| --- | --- |
| `Accept` includes `application/json` | JSON |
| The body is JSON | JSON |
| `Sec-Fetch-Mode: navigate` (a normal form post) | Redirect or hosted page |
| Any other `Sec-Fetch-Mode`, such as `cors` from `fetch` | JSON |
| No `Sec-Fetch-Mode`, and `Accept` includes `text/html` | Redirect or hosted page |
| Anything else, such as curl | JSON |

In JSON mode success is `201 { "ok": true, "id": "sub_..." }`. In page mode success is a `303` redirect, and errors render a hosted error page that lists field problems by label.

## After a submission is stored

| What | When | Details |
| --- | --- | --- |
| Webhooks | Seconds later, retried for about 3 days | [Webhooks](https://ikiform.com/docs/webhooks.md) |
| Notification email | Seconds later, if notifications are on for the form | Capped at 50 emails per form per hour (10 for workspaces younger than 7 days), then one hourly digest |
| Dashboard | Immediately, live | Spam shows under the spam filter |
| REST API | Immediately | [REST API](https://ikiform.com/docs/api.md) |

> Forms whose submissions contain password, card or seed-phrase style field names are flagged and their notification emails held for review. Don't collect credentials with a form backend.

## Ids

| Id | Looks like | Notes |
| --- | --- | --- |
| Form | `frm_xxxxxxxxxxxxxxxx` | `frm_` plus 16 random characters. Public: it is in your form's action URL. |
| Submission | `sub_01k7v2c9t3e8m5q4x6z0a1b2c3` | `sub_` plus 26 characters, ordered by time. |
| Webhook delivery | `dlv_...` | Sent as `webhook-id`; stays the same across retries. |
| Request | `req_...` | Every response has an `X-Request-Id` header. Quote it when you contact support. |

**201** (`JSON success`)

```json
{ "ok": true, "id": "sub_01k7v2c9t3e8m5q4x6z0a1b2c3" }
```

**303** (`Browser success`)

```http
HTTP/1.1 303 See Other
Location: https://example.com/thanks
X-Request-Id: req_01k7v2c9t3e8m5q4x6z0a1b2c3
```
