# BootForm — Full Reference Condensed, LLM-optimized reference. Covers the same REST + MCP surface as https://bootform.com/openapi.json (auto-generated from the real API — if the two ever disagree, openapi.json is the source of truth). See https://bootform.com/llms.txt for a shorter overview. Conventions: all bodies are JSON unless noted. Errors: `{"ok": false, "error": "...", "code": "..."}`. Success responses include `"ok": true`. Pagination: `page` (1-based), `per_page` (default 50, max 200). Timestamps are ISO 8601 UTC. Auth on every endpoint below except the submission endpoint: `Authorization: Bearer {api_key}` (a Dashboard UI session JWT or an account API key — both use the same Bearer scheme, validated differently server-side). --- ## Submission Endpoint (unauthenticated) ### `POST https://f.bootform.com/{form_id}` The only public, unauthenticated endpoint. `form_id` is a UUID or vanity slug (Pro+ plan). Body: `application/x-www-form-urlencoded` or `application/json` (`multipart/form-data` for file uploads isn't implemented yet). Special fields, stripped before storage: `_redirect` (or `_next` alias), `_replyto`, `_subject`, `_format=json`, `_honeypot` (must stay empty). `_language` is parsed but currently has no effect — reserved for future use, don't rely on it. Responses: - `200 {"ok": true}` — accepted (AJAX/JSON) or a browser redirect to the referring page/`_redirect` - `200 {"ok": true, "status": "pending_claim", "claim_url": "https://app.bootform.com/claim/{id}"}` — accepted, form not yet claimed - `422 {"ok": false, "error": "...", "code": "CAPTCHA_FAILED"}` — CAPTCHA failed (if configured) - `422` with a per-field `errors` array — field validation failed (`REQUIRED_FIELD_MISSING`, `TYPE_EMAIL`/`TYPE_URL`/`TYPE_TEL`/`TYPE_NUMBER`/`TYPE_DATETIME`, `FIELD_TOO_SHORT`/`FIELD_TOO_LONG`, `WORK_EMAIL_REQUIRED`). A classic (non-AJAX) form post gets the same rejection as a redirect with `?bootform_error=CODE&bootform_field=name` instead of a JSON body. - `429 {"code": "QUOTA_EXCEEDED"}` — monthly submission quota reached - `403 {"code": "FORM_DISABLED"}` or `{"code": "ORIGIN_NOT_ALLOWED"}` --- ## Forms — base `https://api.bootform.com` ### `GET /forms` — list forms Query: `page`, `per_page`, `workspace_id` (omit to list across every workspace). Response: `{"ok": true, "forms": [...], "total", "page", "per_page"}`. Each form: `id`, `workspace_id`, `name`, `enabled`, `endpoint`, `submission_count`, `monthly_usage`, `monthly_quota`, `created_at`, `updated_at`. ### `POST /forms` — create a form Body: `{"name": "...", "redirect_url"?, "allowed_origins"?: [...], "subject_template"?, "slug"? (Pro+ only, 403 PLAN_LIMIT otherwise), "workspace_id"?}`. `201` response includes `html_snippet` — a ready-to-paste `
` block. ### `GET /forms/{form_id}` — get one form Full object: everything from the list shape plus `slug`, `html_snippet`, `redirect_url`, `allowed_origins`, `allowed_origins_inherited`, `effective_allowed_origins`, `time_zone`, `time_zone_inherited`, `effective_time_zone`, `subject_template`, `email_branding_removed`, `spam_protection: {honeypot, captcha}`, `field_rules`. `*_inherited: true` + `effective_*` means the value comes from the parent workspace's `default_settings`, not the form's own override. `effective_allowed_origins` is what's actually enforced on the submission endpoint. `field_rules` entries: `{field, required?, type? (email|url|tel|number|datetime), min?/max?, min_length?/max_length?, accepted_mime_types?, require_work_email?}`. ### `PATCH /forms/{form_id}` — update a form Any subset of the create fields, plus `enabled`, `email_branding_removed` (Starter+ only), `workspace_id` (moves the form), `field_rules` (full replace, not merged), `spam_protection: {honeypot}` (CAPTCHA is separate, see below), `time_zone` (empty string `""` clears an override back to "inherit"; omitted/null leaves it unchanged). ### `PUT /forms/{form_id}/captcha` — set CAPTCHA (bring-your-own-keys) Body: `{"provider": "turnstile", "site_key": "...", "secret_key": "..."}` — `turnstile` is the only supported provider today. `secret_key` is write-only, never returned by any response. ### `DELETE /forms/{form_id}/captcha` — remove CAPTCHA (no-op if none configured) ### `DELETE /forms/{form_id}` — delete a form and all its submissions (irreversible) --- ## Submissions — base `https://api.bootform.com/forms/{form_id}/submissions` ### `GET /` — list submissions Query: `page`, `per_page`, `q` (substring search across field values), `spam` (default false — set true to view the spam folder instead of the normal list), `archived` (default false, same pattern), `starred`, `since`, `until`. Each submission: `id`, `form_id`, `fields` (object), `files`, `ip`, `spam`, `spam_score`, `read`, `starred`, `archived`, `created_at`. ### `GET /{submission_id}` — get one submission (also marks it read) ### `PATCH /{submission_id}` — update `read`/`starred`/`archived` ### `DELETE /{submission_id}` — delete a submission (GDPR erasure) ### `GET /stats` — daily submission counts Query: `since` (default 30 days ago), `until` (default today). Response: `{"ok": true, "daily": [{"date": "2026-07-01", "count": 12}, ...]}`. --- ## Notification Destinations — base `https://api.bootform.com/forms/{form_id}/destinations` ### `GET /` — list destinations ### `POST /` — add a destination Body: `{"type": "email"|"discord"|"slack"|"telegram"|"webhook", "config": {...}}`. **Only `email` and `discord` actually deliver today** — `slack`/`telegram`/`webhook` are accepted by the API but have no real sender behind them yet. Config shapes: - `email`: `{"address": "..."}` — must be a verified email (see below) - `discord`: `{"webhook_url": "...", "username"?, "avatar_url"?, "color"?}` - `webhook`: `{"url": "...", "headers"?}` — response includes `config.signing_secret` in plaintext exactly once on creation; read it again later via `rotate-secret` `201` on Free plan email limit exceeded → `403 {"code": "EMAIL_LIMIT_REACHED"}`. ### `PATCH /{destination_id}` — enable/disable or update config ### `POST /{destination_id}/rotate-secret` — webhook only, `404` on any other type Generates a new signing secret immediately (no overlap window). Response: `{"ok": true, "signing_secret": "whsec_..."}`, shown in plaintext exactly once. ### `DELETE /{destination_id}` — remove a destination --- ## Verified Emails — base `https://api.bootform.com/account/verified-emails` An email destination must point at a verified address — verify it here first, then reuse it across any form's `email`-type destination. ### `GET /` — list this account's verified emails (`id`, `email`, `verified_at`, `created_at`) ### `POST /` — start verification for a new email Body: `{"email": "..."}` — must contain a dotted domain (e.g. `a@b.com`, not `a@b`). Sends a verification link to that address. ### `POST /{id}/resend` — resend the verification link ### `DELETE /{id}` — remove a verified email ### `GET /verify/{token}` — unauthenticated; the token itself (emailed) proves ownership --- ## Account — base `https://api.bootform.com` ### `GET /account` — the authenticated account's profile `{"ok": true, "account": {id, email, name, avatar_url, oauth_provider, plan, plan_expires_at, monthly_usage, monthly_quota, created_at}}`. `avatar_url`/`oauth_provider` are null for password-only accounts and Microsoft OAuth logins. ### `GET /account/stats` — account-wide dashboard stats `{"ok": true, "total_submissions", "submissions_last_30_days", "spam_blocked_last_30_days", "delivery_status": {...}, "country_breakdown": [...]}`. ### `GET /account/api-keys` — list API keys (label + prefix only, never the full key) ### `POST /account/api-keys` — create a key Body: `{"label": "..."}`. Response includes the full key **exactly once**: `{"api_key": {..., "key": "bf_live_..."}, "warning": "Store this key securely. It will not be shown again."}`. ### `DELETE /account/api-keys/{key_id}` — revoke a key immediately --- ## Auth — base `https://api.bootform.com/auth` Password and magic-link login both return the same shape: `{"ok": true, "access_token": "...", "expires_in": 3600, "refresh_token": "..."}`. - `POST /register` — `{"email", "password" (min 8 chars), "name"?, "time_zone"?}` - `POST /login` — `{"email", "password"}` - `POST /refresh` — `{"refresh_token"}` - `POST /logout` — `{"refresh_token"}` → `{"ok": true}` - `POST /magic-link` — `{"email"}` → `{"ok": true}` (sends a login link, no password needed) - `GET /magic-link/{token}` — redeem the emailed link → same auth-response shape as login - `GET /oauth/{provider}` — `provider` is `github`/`google`/`microsoft`; redirects to the provider - `GET /oauth/{provider}/callback?code&state` — completes OAuth, same auth-response shape --- ## Workspaces — base `https://api.bootform.com/workspaces` Group forms and inherit shared defaults (`allowed_origins`, `time_zone`). Free/Starter capped at one workspace (`403 WORKSPACE_LIMIT` on a second). ### `GET /` — list workspaces (`id`, `name`, `is_default`, `form_count`, `default_settings`, `created_at`, `updated_at`) ### `POST /` — create a workspace Body: `{"name", "time_zone"?}`. ### `PATCH /{workspace_id}` — update name/default_settings ### `DELETE /{workspace_id}` — delete a workspace; `403 CANNOT_DELETE_DEFAULT_WORKSPACE` on the account's default one (rename it instead) --- ## Claim Flow — unauthenticated preview, authenticated claim ### `GET /claim/{form_id}/preview` — unauthenticated `{"ok": true, "form_id", "held_count", "preview": [up to 3 most recent held submissions, full field data]}`. `held_count: 0, preview: []` if nothing is held (never submitted to, or already claimed) — not an error. ### `POST /claim/{form_id}` — authenticated Creates the real `forms` row for a previously-unclaimed `form_id`. `201 {"ok": true, "form_id", "name": "Untitled Form"}`. `409 {"code": "ALREADY_CLAIMED"}` on a second attempt — not idempotent by design. --- ## Error codes `UNAUTHORIZED` (401) missing/invalid key · `FORBIDDEN`/`NOT_FOUND` (403/404) · `FORM_DISABLED` (403) · `ORIGIN_NOT_ALLOWED` (403) · `CAPTCHA_FAILED` (422) · `QUOTA_EXCEEDED`/`RATE_LIMITED` (429) · `EMAIL_LIMIT_REACHED` (403) · `PLAN_LIMIT` (403, see pricing) · `VALIDATION_ERROR` (422) · `INTERNAL_ERROR` (500) · `ALREADY_CLAIMED` (409) · `REQUIRED_FIELD_MISSING`/`TYPE_EMAIL`/`TYPE_URL`/ `TYPE_TEL`/`TYPE_NUMBER`/`TYPE_DATETIME`/`FIELD_TOO_SHORT`/`FIELD_TOO_LONG`/`WORK_EMAIL_REQUIRED` (422, field validation) · `WORKSPACE_LIMIT`/`CANNOT_DELETE_DEFAULT_WORKSPACE` (403). ## Rate limits Per-minute request limits by plan: Free 60, Starter 300, Pro 1,000, Business 5,000. Every response carries `X-RateLimit-Limit`/`X-RateLimit-Remaining`/`X-RateLimit-Reset` headers. --- ## MCP (Model Context Protocol) Server **Endpoint:** `https://mcp.bootform.com/sse`. **Auth:** an account API key as `Authorization: Bearer {api_key}` or `?api_key={api_key}` in the connection URL. ### `bootform_create_form` Create a new form. Returns the endpoint URL and a ready-to-use HTML snippet. ### `bootform_configure_form` Update an existing form's settings (name, redirect URL, allowed origins, etc. — same fields as `PATCH /forms/{form_id}`). ### `bootform_list_forms` List the caller's forms. ### `bootform_get_snippet` Get a fresh HTML embed snippet for a form, with an arbitrary set of fields. ### `bootform_add_email` Add an email notification destination to a form. The email must already be verified on the account (see Verified Emails above) — this tool doesn't itself trigger email verification. ### `bootform_add_discord` Add a Discord notification destination (a Discord incoming-webhook URL) to a form. No other channel types are wired up to MCP tools yet — matches the REST API's own current limitation (only email and Discord actually deliver).