send.webforms.toRead the docs →

Documentation

Write API

Manage a workspace end to end without opening the dashboard - built for scripts and AI agents that need to create and configure forms, not just read submissions from them. Available on the Pro and Agency plans, same as the Submissions API.

Scopes

Every key defaults to read-only. Write access is a set of opt-in scopes you choose when creating the key in Settings → API keys:

  • forms:writeCreate, update and delete forms.
  • destinations:writeAdd, update, pause and delete destinations.
  • routes:writeCreate, update, reorder and delete routing rules.
  • templates:writeCreate, update and delete custom notification templates.
  • api_keys:writeCreate and revoke other API keys - never more powerful than the key that creates them.
  • members:writeRemove and change the role of team members, and rename the workspace. Sending invitations stays dashboard-only.

A key minted before a scope existed keeps its original permissions - scopes are fixed at creation, not retroactive. Create a new key to pick up new scopes.

Forms

Terminal
curl -X POST https://api.webforms.to/v1/forms \
  -H "Authorization: Bearer wf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "Contact form"}'

POST /v1/forms, PATCH /v1/forms/:id and DELETE /v1/forms/:id. A new form gets a live endpoint and, if your workspace has a verified default email address, an email destination attached automatically - the same zero-friction default the dashboard gives a form created by hand.

The PATCH body is a partial update - send only the fields you want to change:

  • name, description, isActiveBasic fields.
  • spamConfigAny subset of the spam settings - see spam protection.
  • uploads{ allowFileUploads, maxFileSizeMb, allowedFileTypes } - see file uploads.
  • confirmation{ redirectUrl, successMessage, thankYouTitle, forceJson, customJsonSuccess, customJsonError } - what happens after a browser submit. redirectUrl and successMessage are free on every plan; the rest are Pro and Agency. customJsonSuccess/customJsonError are flat string key/value objects merged into the JSON response, up to 20 entries.
  • notificationTemplateIdWhich notification email template renders for this form - a built-in id or a custom template's id.
  • autoResponder{ enabled, subject, body, fromName } - the confirmation email sent back to the person who submitted.
  • allowedOriginsArray of hostnames (or *.example.com wildcards) allowed to submit to this form. Pro and Agency.

Every constraint the dashboard enforces - plan limits, blocked upload extensions, redirect URL validation, template plan gating - is enforced here too, from the same shared code. There is no separate, looser API path.

Destinations

GET/POST /v1/forms/:formId/destinations and PATCH/DELETE .../destinations/:id. The create/update body is { provider, fields } where provider is one of email, slack, discord, telegram or webhook.

Terminal
curl -X POST https://api.webforms.to/v1/forms/frm_xxx/destinations \
  -H "Authorization: Bearer wf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"provider": "slack", "fields": {"webhookUrl": "https://hooks.slack.com/services/..."}}'

Email destinations must point at a verified linked address for the workspace; webhook URLs cannot target private or loopback hosts (SSRF protection); credentials (bot tokens, webhook secrets) are write-only and never returned by any endpoint.

To pause or resume a destination without touching its configuration, PATCH with just isActive:

Terminal
curl -X PATCH https://api.webforms.to/v1/forms/frm_xxx/destinations/int_xxx \
  -H "Authorization: Bearer wf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"isActive": false}'

Routing rules

GET/POST /v1/forms/:formId/routes, PATCH/DELETE .../routes/:id and POST .../routes/:id/reorder, on Pro and Agency. Full syntax and examples on the routing rules page.

Custom email templates

GET/POST /v1/templates and PATCH/DELETE /v1/templates/:id, on plans with premium email templates. Select one on a form through notificationTemplateId in PATCH /v1/forms/:id.

Terminal
curl -X POST https://api.webforms.to/v1/templates \
  -H "Authorization: Bearer wf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "Support ticket", "subject": "New ticket: {{subject}}", "html": "<p>{{message}}</p>"}'

Generate a draft instead of writing HTML by hand with POST /v1/templates/generate - the same monthly allowance the dashboard's “Generate” button draws from, counted from the same place, so an agent and a human sharing a workspace can't double the effective quota between them:

Terminal
curl -X POST https://api.webforms.to/v1/templates/generate \
  -H "Authorization: Bearer wf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "A clean, minimal notification email for a contact form."}'

Returns { subject, html } - review it, then POST /v1/templates to save it if you like it. Generating doesn't save anything by itself and doesn't count against your saved-template limit, only against the monthly generation allowance.

API keys

A key with the api_keys:write scope can list, create and revoke other keys in the workspace over GET/POST /v1/api-keys and DELETE /v1/api-keys/:id - useful for an agent that provisions its own scoped sub-keys for different jobs instead of sharing one broad key everywhere.

One rule this endpoint enforces that the dashboard doesn't need to: a key can never mint another key with a scope it doesn't itself hold. Ask for a scope you don't have and it is silently left off the new key, rather than erroring and confirming which scopes exist. This is what keeps api_keys:write from being a privilege-escalation primitive - a leaked key can hand out copies of its own access, never more.

Terminal
curl -X POST https://api.webforms.to/v1/api-keys \
  -H "Authorization: Bearer wf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "nightly-report-bot", "writeScopes": ["forms:write"]}'

The raw key is returned once, in the response to POST, and never again - store it immediately, the same guarantee the dashboard gives when you create a key by hand.

Team members

A key with the members:write scope can list members and pending invitations (GET /v1/members), cancel a pending invitation (DELETE /v1/invitations/:id), change an existing member's role, or remove them (PATCH/DELETE /v1/members/:id).

Terminal
curl -X PATCH https://api.webforms.to/v1/members/mem_xxx \
  -H "Authorization: Bearer wf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"role": "admin"}'

role is admin or member - both endpoints refuse to touch the workspace owner (cannot_change_owner / cannot_remove_owner), the same restriction the dashboard applies.

Sending a new invitation stays dashboard-only, deliberately - it's the one write action in this whole API that sends an email, and that path is kept out of API reach entirely rather than gated behind a scope. Invite teammates from Settings → Team in the dashboard.

Workspace settings

GET /v1/workspace returns the workspace's name, slug and plan. PATCH /v1/workspace renames it or changes its slug - send only the field you want to change, same scope as team members (members:write) since both are "administer this workspace" actions on the dashboard too.

Terminal
curl -X PATCH https://api.webforms.to/v1/workspace \
  -H "Authorization: Bearer wf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "Acme Inc"}'

Slugs are normalized to lowercase letters, numbers and hyphens, and must be unique across every workspace on WebForms - the same check the dashboard runs before saving one.

What's deliberately not here

Four things intentionally stop at the dashboard rather than being exposed over the API. A single person's own account - display name, password, email verification - has no API equivalent, because an API key authenticates a workspace, not a logged-in human; there is no "current user" for a key to act as. Sending a team invitation is dashboard-only too - it's a write action that would send an email, so that path stays out of API reach entirely rather than being gated behind a scope. Billing - starting a checkout, opening the customer portal - is dashboard-only as well: spending the workspace's money is not something an API key gets to do, full stop, not even behind an opt-in scope. And MCP tools for AI agents remain read-only by design - a model that reads untrusted submission content (which can contain injected instructions) should never also hold write or delete power over the workspace. The REST write API documented on this page is the intended path for an agent that genuinely needs to configure things, using a scoped key it controls directly rather than one handed to a model's tool-calling loop.

Rate limits

120 requests per minute per key, the same ceiling as the read API. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, and going over it returns a 429. Reads and writes on the same key share one counter, not separate budgets.

Tenant isolation

Every write endpoint is scoped to the key's workspace, exactly like the read API - there is no endpoint that accepts an organization id, so cross-tenant writes are not a validation step that could be forgotten, they are structurally impossible. A request naming a real form id from a different workspace gets an identical 404 to a made-up id - the API never confirms or denies that a resource exists outside your own workspace.

Get your endpoint

Free for 300 submissions a month. No card required.

Start free