# Submitting data

> POST to a form endpoint from a browser, a server, or an agent, and what comes back.

Once a form is [provisioned](/docs/forms/provisioning), anything can post to its `endpoint_url`. This page is the contract for that endpoint.

## The request

```text
POST /v1/f/{form_id}
```

Accepted content types:

- `application/json`: a flat object of field values. A non-object body is rejected with `400 invalid_body`.
- `application/x-www-form-urlencoded`: standard form encoding.
- `multipart/form-data` **without file parts**. A file part returns `415 files_not_supported`; there are no uploads in v1. Repeated keys collapse to a list.

A request with **no `Content-Type`** is rejected with `415 unsupported_content_type` rather than being silently dropped.

```bash
curl -X POST https://api.lanes.sh/v1/f/YOUR_FORM_ID \
  -H "Content-Type: application/json" \
  -d '{"email": "visitor@site.com", "message": "Hello!"}'
```

## The response

The endpoint serves both machines and browsers, and it decides which you are from the `Accept` header:

- **API callers** (Accept is not `text/html`) get `200` with a JSON receipt:

  ```json
  { "ok": true, "submission_id": "b7c2..." }
  ```

- **Browser form posts** (Accept includes `text/html`) get a `303` redirect to `/thanks/{form_id}`, so a plain HTML form lands the visitor on a thank-you page with no JavaScript.

## Fields and validation

The form's `schema` defines the known fields. On submit, values are split into `data` (keys that match the schema) and `extra` (everything else). Extra keys are stored, not rejected, so adding a field to your markup will not break submissions.

Required fields that are absent or empty return `422 missing_required_fields`, and the response lists exactly which fields were missing.

## The honeypot

Every form reserves a field named `_gotcha`. It should be present in your markup but hidden from humans and left empty. If it arrives filled, the submission is silently marked as spam and still returns success, so a bot learns nothing. `_gotcha` is stripped before storage and cannot appear in a form's schema.

```html
<input type="text" name="_gotcha" style="display:none" tabindex="-1" />
```

## Limits

| Guard | Limit | Error |
| --- | --- | --- |
| Body size | 64 KB | `413 payload_too_large` |
| Rate limit | 10 per minute per IP | `429 submission_rate_limited` |
| Unclaimed cap | 25 stored submissions | `429 unclaimed_submission_cap` |

**The unclaimed cap**

While a form is unclaimed it stores up to 25 submissions (spam counts toward the total). At the cap, a genuine submission is refused with `429 unclaimed_submission_cap` until the owner [claims the form](/docs/forms/claiming). Over-cap spam still returns success but is not stored.

## What is stored

Each stored submission keeps the `data` and `extra` field values, the request `origin`, a **salted hash of the IP** (the raw IP is never stored), a truncated user agent, a spam score, and whether it has been `released` to the dashboard. Submissions collected while unclaimed are held until a claim releases them.

## State gates

If a form has been deleted or has frozen from expiry, its endpoint returns `410`. An unknown `form_id` returns `404 form_not_found`.

Next: [Integrating a form](/docs/forms/integrating) for ready-made snippets, and [Securing your endpoints](/docs/forms/securing-your-endpoints) for origins and keys.
