Docs/Forms/Guides/Provisioning a form

Provisioning a form

Create a live form endpoint with a single POST, anonymously or with a workspace key.

View as Markdown

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
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"}
    ]
  }'
FieldTypeNotes
schemaarrayRequired. One or more fields. Each has name, type, optional required and max_length.
recipientsarrayAddresses submissions forward to (max 5). The first is where a claim link is sent; each must be verified before forwarding reaches it.
allowed_originsarrayBrowser origins allowed to submit, for example ["https://example.com"]. Empty means no browser origin is accepted.
namestringA label for the form in the dashboard.
workflowarrayDelivery actions (email, store, webhook). Defaults to email plus Lanes storage.
forward_email / store_submissionsbooleanShorthand toggles when you do not want to pass a full workflow.
metadataobjectArbitrary 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.

json
{
  "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_to is set and claim_url is null.
  • Without any recipients, no email is sent. Instead the response returns a single-use claim_url for 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
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 the frozen state.

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:

bash
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:

GuardLimitError
Disposable email addressesrejected422 disposable_email
Forms per IP per 24h20rate limited
Forms per email domain per 24h100rate limited
Pending unclaimed form for the same emailone at a time429 pending_claim_exists
Claim emails per address per 24h5rate limited

Keyed creates skip these gates, since the workspace key already establishes trust.

Form states

A form moves through a small state machine:

  • unclaimed to claimed: someone claims the form.
  • unclaimed to frozen: 7 days pass with no claim. The endpoint then returns 410.
  • frozen to claimed: claiming from a reminder link restores a frozen form.
  • claimed to deleted: the owner deletes it. The endpoint returns 410.
  • frozen or deleted to purged: 30 days later the data is hard-deleted.

Next: Submitting data and Integrating a form.