Provisioning is how a form comes to life. One POST /v1/forms returns a live endpoint that starts accepting submissions immediately. There is no signup, and the same call works from a coding agent over MCP or from a raw curl.
The request
Only schema is required. Everything else is optional.
curl -X POST https://api.lanes.sh/v1/forms \
-H "Content-Type: application/json" \
-d '{
"recipients": ["you@company.com"],
"allowed_origins": ["example.com"],
"schema": [
{"name": "email", "type": "email", "required": true},
{"name": "message", "type": "textarea"}
]
}'| Field | Type | Notes |
|---|---|---|
schema | array | Required. One or more fields. Each has name, type, optional required and max_length. |
recipients | array | Addresses submissions forward to (max 5). The first is where a claim link is sent; each must be verified before forwarding reaches it. |
allowed_origins | array | Browser origins allowed to submit, for example ["https://example.com"]. Empty means no browser origin is accepted. |
name | string | A label for the form in the dashboard. |
workflow | array | Delivery actions (email, store, webhook). Defaults to email plus Lanes storage. |
forward_email / store_submissions | boolean | Shorthand toggles when you do not want to pass a full workflow. |
metadata | object | Arbitrary string key-value pairs stored with the form. |
Field types are text, email, textarea, number, checkbox, and hidden.
What you get back
A create returns 201; see Idempotency for the one case that returns 200.
{
"form_id": "e5b1c0de-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
"endpoint_url": "https://api.lanes.sh/v1/f/e5b1c0de-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
"claim_url": null,
"state": "unclaimed",
"claim_email_sent_to": "you@company.com",
"expires_at": "2026-07-13T09:00:00Z",
"limits": { "unclaimed_max_submissions": 25 },
"workflow": [
{ "type": "email", "enabled": true, "to": ["you@company.com"] },
{ "type": "store", "enabled": true, "destination": "lanes" },
{ "type": "webhook", "enabled": false, "url": null }
],
"forward_email": true,
"store_submissions": true,
"idempotent_replay": false
}Point your site at endpoint_url and you are collecting. See Integrating a form.
Anonymous versus keyed
How you authenticate the create call decides the form's starting state.
Anonymous (no auth)
The form is born unclaimed. It collects right away but holds everything until someone claims it.
- With one or more
recipients, a claim link is emailed to the first one.claim_email_sent_tois set andclaim_urlisnull. - Without any
recipients, no email is sent. Instead the response returns a single-useclaim_urlfor you to hand to the form's owner.
Keyed (workspace API key)
Send a workspace key and the form is born claimed directly into that key's workspace. No claim email is sent, no claim step is needed, and claim_url, claim_email_sent_to, and expires_at are all null.
curl -X POST https://api.lanes.sh/v1/forms \
-H "Authorization: Bearer lfk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": ["you@company.com"],
"allowed_origins": ["example.com"],
"schema": [
{"name": "email", "type": "email", "required": true},
{"name": "message", "type": "textarea"}
]
}'See API keys to create one.
Limits while unclaimed
An unclaimed form is a holding pen, not a dead end:
- It stores up to 25 submissions (
limits.unclaimed_max_submissions). - It forwards nothing. Email delivery turns on only when the form is claimed.
- It expires after 7 days (
expires_at) if never claimed, moving to thefrozenstate.
Claim to keep collecting
Claiming lifts these limits, releases every held submission into the dashboard, and turns on forwarding. See Claiming a form.
Idempotency
Every create makes a new form. Two identical calls give you two independent forms, each with its own claim link: the same schema is not the same form.
To make a retry safe, send an Idempotency-Key header with a value you generate per call, and reuse that value if you retry:
curl -X POST https://api.lanes.sh/v1/forms \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0" \
-d '{"schema":[{"name":"email","type":"email","required":true}]}'Repeating a call with the same key within 24 hours replays the original form and returns 200 with idempotent_replay: true instead of creating a second one. A replay never re-issues the claim link (claim_url is null, and no claim email is re-sent), so keep the link from the original response. Keys are scoped to the caller, so yours never collides with anyone else's. Keyed (lfk_) creates always create a new form.
Anti-abuse
The anonymous path is rate limited and gated so it cannot be used to spam inboxes:
| Guard | Limit | Error |
|---|---|---|
| Disposable email addresses | rejected | 422 disposable_email |
| Forms per IP per 24h | 20 | rate limited |
| Forms per email domain per 24h | 100 | rate limited |
| Pending unclaimed form for the same email | one at a time | 429 pending_claim_exists |
| Claim emails per address per 24h | 5 | rate limited |
Keyed creates skip these gates, since the workspace key already establishes trust.
Form states
A form moves through a small state machine:
unclaimedtoclaimed: someone claims the form.unclaimedtofrozen: 7 days pass with no claim. The endpoint then returns410.frozentoclaimed: claiming from a reminder link restores a frozen form.claimedtodeleted: the owner deletes it. The endpoint returns410.frozenordeletedtopurged: 30 days later the data is hard-deleted.
Next: Submitting data and Integrating a form.