For agents

FormSecured for agents

Build a form, publish it, send the link, read the answers back. Three ways in - MCP, plain HTTP, or a single-file CLI - over the same operations the browser UI uses.

MCP server

One stateless JSON-RPC endpoint. No session, no handshake to keep open: each request carries the key.

POST https://formsecured.com/mcp
Authorization: Bearer fsk_...
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"list_forms","arguments":{}}}

Methods: initialize, ping, tools/list, tools/call. A rejected tool answers isError: true with a plain-text reason in the content block.

Getting a key

An agent starts with nothing and gets credentials from the human in a browser, device-grant style. Both steps below are public - no key required.

  1. request_agent_authorization with your agent name. Returns a deviceCode, a short userCode, and a verifyUrl.
  2. Open verifyUrl in the human's browser. They sign in, see the plan and your agent name, and click Authorize agent.
  3. poll_agent_authorization with the deviceCode. It answers pending until they approve, then claimed with the key - exactly once. A second poll returns claimed with no key.

Keys start fsk_ and are stored hashed. Approving needs an active subscription; the human sees a checkout button if there is none.

Tools

Nineteen tools, each mirrored by a button in the UI. Anything an agent can do here, a person can do in the browser, and the other way round.

ToolWhat it does
request_agent_authorizationStart the device grant; public.
poll_agent_authorizationCollect the key once the human approves; public.
list_templatesBuilt-in templates plus the ones you saved.
list_formsYour forms, newest first, with draft or published status.
get_formOne form with its fields and settings.
get_form_embedThe iframe snippet, its URL and its height.
create_formA draft from a title and explicit fields.
create_form_from_templateA draft copied from a template.
update_formPatch a form; only the keys you send change.
publish_formMake the public path accept submissions.
unpublish_formTake it back to draft.
delete_formDelete a form and its submissions.
save_form_as_templateKeep a form's fields as a reusable template.
list_submissionsAnswers for one form, newest first; limit and offset page it, the reply carries total and hasMore.
export_submissionsCSV or JSON, up to 10000 rows, as a file: an embedded resource with the bytes, plus the URL the same key can download it from.
submit_formAnswer a published form; marked agent.
list_agent_keysThis account's keys, never the secrets.
revoke_agent_keyKill one key immediately.
delete_accountDelete the account and everything it owns; needs confirm: true.

HTTP API

Every tool is one route, so anything without an MCP client can call the API directly with a fsk_ key.

RouteWhat it does
GET /api/accountThe account and its billing status.
GET /api/templatesTemplates you can build from.
GET|POST /api/formsList, or create a draft.
GET|PATCH|DELETE /api/forms/:idRead, patch, delete one form.
POST /api/forms/:id/publish|unpublishPublish state.
GET /api/forms/:id/embedThe embed snippet, URL and height.
GET /api/forms/:id/submissionsAnswers; ?limit=&offset= to page, ?includeSpam=1 to see flagged rows.
GET /api/forms/:id/submissions/exportCSV or JSON file (content-disposition: attachment), ?format=csv|json.
GET /api/public/forms/:slugA published form's definition; public.
POST /api/public/forms/:slug/submissionsAnswer a form; public.
GET /api/keys, DELETE /api/keys/:idList and revoke agent keys.

Errors are {"errors":[{"field":"…","message":"…"}]} with a real status code - 400 for a bad input, 401 without a key, 404 for something you do not own, 429 with Retry-After when a budget is spent.

CLI

One file, no dependencies, Node 18+. It talks to the HTTP API, so it needs no MCP client.

curl -fsSL https://formsecured.com/cli.mjs -o fs.mjs
export FORMSECURED_API_KEY=fsk_...

node fs.mjs forms list
node fs.mjs forms embed frm_...
node fs.mjs submit contact --set name=Ada --set email=ada@example.com
node fs.mjs submissions export frm_... --format csv -o answers.csv

JSON on stdout, diagnostics on stderr, and exit codes you can branch on: 0 success, 1 API error, 2 usage, 3 unauthenticated, 4 rate limited. --help is the whole manual.

Embedding a form

get_form_embed returns an iframe snippet, the frame URL and a starting height. The frame measures itself and reports its height to the host page. Publish the form first: an unpublished embed answers 404.

Limits and guards

Submissions from a key are identified by that key, so they skip the proof-of-work puzzle an anonymous browser solves. Budgets apply either way: 100 submissions an hour per key, 200 an hour per form, 20 per ten minutes per address. A spent budget is the only hard refusal, and it comes with Retry-After. Suspicious answers are stored and flagged, not rejected - a false positive stays recoverable.

Price

$10 a year, one plan, no tiers. Accounts on the internal allowlist are exempt. Payment is a browser action: the human approves the agent after checkout, and the agent never handles a card.

The browser UI does all of the same work - create an account, or see what FormSecured is.