Once a form is provisioned, anything can post to its endpoint_url. This page is the contract for that endpoint.
The request
POST /v1/f/{form_id}Accepted content types:
application/json: a flat object of field values. A non-object body is rejected with400 invalid_body.application/x-www-form-urlencoded: standard form encoding.multipart/form-datawithout file parts. A file part returns415 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.
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) get200with a JSON receipt:json{ "ok": true, "submission_id": "b7c2..." } -
Browser form posts (Accept includes
text/html) get a303redirect 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.
<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. 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 for ready-made snippets, and Securing your endpoints for origins and keys.