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.
request_agent_authorizationwith your agent name. Returns adeviceCode, a shortuserCode, and averifyUrl.- Open
verifyUrlin the human's browser. They sign in, see the plan and your agent name, and click Authorize agent. poll_agent_authorizationwith thedeviceCode. It answerspendinguntil they approve, thenclaimedwith the key - exactly once. A second poll returnsclaimedwith 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.
| Tool | What it does |
|---|---|
request_agent_authorization | Start the device grant; public. |
poll_agent_authorization | Collect the key once the human approves; public. |
list_templates | Built-in templates plus the ones you saved. |
list_forms | Your forms, newest first, with draft or published status. |
get_form | One form with its fields and settings. |
get_form_embed | The iframe snippet, its URL and its height. |
create_form | A draft from a title and explicit fields. |
create_form_from_template | A draft copied from a template. |
update_form | Patch a form; only the keys you send change. |
publish_form | Make the public path accept submissions. |
unpublish_form | Take it back to draft. |
delete_form | Delete a form and its submissions. |
save_form_as_template | Keep a form's fields as a reusable template. |
list_submissions | Answers for one form, newest first; limit and offset page it, the reply carries total and hasMore. |
export_submissions | CSV 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_form | Answer a published form; marked agent. |
list_agent_keys | This account's keys, never the secrets. |
revoke_agent_key | Kill one key immediately. |
delete_account | Delete 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.
| Route | What it does |
|---|---|
GET /api/account | The account and its billing status. |
GET /api/templates | Templates you can build from. |
GET|POST /api/forms | List, or create a draft. |
GET|PATCH|DELETE /api/forms/:id | Read, patch, delete one form. |
POST /api/forms/:id/publish|unpublish | Publish state. |
GET /api/forms/:id/embed | The embed snippet, URL and height. |
GET /api/forms/:id/submissions | Answers; ?limit=&offset= to page, ?includeSpam=1 to see flagged rows. |
GET /api/forms/:id/submissions/export | CSV or JSON file (content-disposition: attachment), ?format=csv|json. |
GET /api/public/forms/:slug | A published form's definition; public. |
POST /api/public/forms/:slug/submissions | Answer a form; public. |
GET /api/keys, DELETE /api/keys/:id | List 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.