form·vee API
Public developer manual for the form·vee REST API and MCP server.
Use workspace API keys from /settings/api-keys to create and publish forms,
and to read completed responses from scripts or AI agents.
Getting started
- Sign in and open
/settings/api-keys. - Create a key with only the scopes your client needs.
- Copy the plaintext key once (
fv_<prefix>_<secret>). It is never shown again. -
Call REST at
https://form.vee.kr/api/v1or connect an MCP client tohttps://form.vee.kr/api/mcp.
Authentication
Workspace API key. Available scopes: forms:read, forms:write, forms:publish, responses:read, responses:partial:read, assets:write, files:read.
Send the key on every request:
Authorization: Bearer fv_<prefix>_<secret>
OpenAPI JSON at /api/v1/openapi.json is the only unauthenticated public
machine-readable surface (CORS *). All other /api/v1 and
/api/mcp routes require a valid Bearer key.
Scopes
Scopes are granted at key creation time. Missing scope returns 403 forbidden.
Response creation is not available over API or MCP; respondents submit through the public form.
forms:readforms:writeforms:publishresponses:readresponses:partial:readassets:writefiles:read
responses:partial:readis required in addition toresponses:readto list in-progress partials (REST only).assets:writeuploads authoring images.files:readdownloads respondent uploads.- MCP never exposes custom script editing or partial response reads.
Errors & rate limits
Authenticated JSON errors use:
{
"ok": false,
"error": {
"code": "conflict",
"message": "Human-readable summary",
"details": {}
}
}
Common codes: unauthorized, forbidden, not_found,
invalid, conflict, unprocessable,
rate_limited, payload_too_large, unsupported_kind,
internal.
Successful authenticated responses include X-RateLimit-Limit,
X-RateLimit-Remaining, X-RateLimit-Reset, and
X-Request-Id. Defaults are 120 requests/minute per key and 600 auth
attempts/minute per IP. 429 responses also include Retry-After.
Inspect remaining budget with GET /api/v1/usage.
REST quick start
List forms:
curl -sS \
-H "Authorization: Bearer $FORMVEE_API_KEY" \
"https://form.vee.kr/api/v1/forms"
Create a classic draft, then publish with the returned revision:
curl -sS -X POST \
-H "Authorization: Bearer $FORMVEE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind":"classic","title":"NPS survey"}' \
"https://form.vee.kr/api/v1/forms"
# After editing the draft definition via PATCH, publish:
curl -sS -X POST \
-H "Authorization: Bearer $FORMVEE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"expectedDraftRevision":1}' \
"https://form.vee.kr/api/v1/forms/<form-id>/publish"
Success bodies are wrapped as { "data": ... } except DELETE (204),
CSV streams, file redirects (302), and the raw OpenAPI document.
REST endpoints
Paths below are relative to https://form.vee.kr/api/v1.
Required scopes come from the OpenAPI x-scope extension used by the runtime.
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /capabilities |
auth only | Returns the calling key's scopes and the workspace-evaluated form kinds, render modes, block types, and allowlisted authoring features. A valid key is sufficient; each mutation still enforces its own scope and feature gates. |
| GET | /usage |
auth only | Any valid key (no specific scope). Returns the key's scopes, the current per-minute rate-limit budget (same values as the X-RateLimit-* response headers), and workspace request counts over the last 24h/7d. |
| GET | /forms |
forms:read |
Workspace forms returned as `{ items, nextCursor }`, ordered by updatedAt/id descending. Search matches title or slug; kind and status are optional exact filters. |
| POST | /forms |
forms:write |
`kind` selects classic (default) or interview; `definition` is validated against the matching schema (FormDefinition for classic, InterviewDefinition for interview). `definition`/`settings` are optional; omitting them starts an empty draft of that kind. A classic definition that uses a feature not enabled for the workspace (file_upload/ranking/matrix) is rejected with 422. |
| GET | /forms/{id} |
forms:read |
Single form with its draft definition and settings. Supports both kinds; `draftDefinition` is a FormDefinition (classic) or InterviewDefinition (interview). |
| PATCH | /forms/{id} |
forms:write |
Optimistic concurrency. Classic forms use FormDefinition; interview forms use InterviewDefinition. Returns 409 on revision conflict. |
| DELETE | /forms/{id} |
forms:write |
Soft delete. |
| GET | /forms/{id}/versions |
forms:read |
Bounded immutable publication history returned as `{ items, nextCursor }` and ordered by version descending. Definitions are omitted; fetch a version detail by id to read its snapshot. |
| GET | /forms/{id}/versions/{versionId} |
forms:read |
Returns the immutable definition used by responses pinned to this formVersionId. |
| POST | /forms/{id}/duplicate |
forms:write + forms:read |
Requires both forms:write and forms:read. Creates a draft of the same kind after reapplying the canonical schema, current workspace feature gates, settings gates, and image-ownership checks. Legacy readable drafts are canonicalized before the new write. |
| POST | /forms/{id}/publish |
forms:publish |
Supports both kinds. Empty forms (classic without input blocks, interview without content) return 422. |
| POST | /forms/{id}/unpublish |
forms:publish |
Unpublish a form |
| PATCH | /forms/{id}/slug |
forms:write |
Requires the `forms.custom-slug` feature. |
| PATCH | /forms/{id}/settings |
forms:write |
Replaces theme, completion, submission, and document settings using optimistic concurrency. Pass the settingsSignature returned by GET /forms/{id} or the preceding successful write. |
| POST | /forms/{id}/assets |
assets:write |
Uploads one image for this form. The server validates PNG, JPEG, GIF, or WebP by magic bytes and caps the file at 5 MiB. The returned key may be used by image blocks, choice images, logos, and document covers. |
| GET | /forms/{id}/responses |
responses:read |
Kind-aware, filtered keyset page returned as `{ items, nextCursor }`. Compact is the default and never includes interview transcript text; use projection=full for answers/files or transcript. status=partial additionally requires `responses:partial:read` and the partial-submissions feature. Partial responses are a live mutable view, so cursor traversal is best-effort rather than snapshot-isolated. |
| GET | /forms/{id}/reports |
responses:read |
Classic forms return per-question and forms.scoring score summaries plus optional analytics.basic aggregates. Interview forms return the current V2 funnel, analysis coverage, and qualitative-code aggregates. Classic text summaries can contain recent answer samples for responses:read clients; interview reports never contain transcript text. The q filter applies to classic question summaries, while analytics remains the selected date cohort. |
| GET | /forms/{id}/responses/summary |
responses:read |
Per-question aggregates plus numeric score summaries when forms.scoring is enabled. Classic forms only; interview forms return 409 (use GET /forms/{id}/reports or /forms/{id}/interview-analytics for Interview V2). |
| GET | /forms/{id}/responses/{responseId} |
responses:read |
Classic forms return a Response; interview forms return an InterviewResponse. |
| GET | /forms/{id}/responses.csv |
responses:read |
Exports completed responses. Classic forms export stable block-id variables and support label/code values, forms.scoring adds stable numeric score_<id> variables, wide/long layout, and a conservative deidentified projection. File link columns point to the API file download route; following those links requires `files:read`. Interview forms export conversation summary, extracted fields, qualitative themes, and transcript. |
| GET | /forms/{id}/insights |
responses:read |
Aggregates AI-extracted insights from completed classic responses into field-promotion candidates (frequency × confidence). `auto` candidates have crossed the auto-promotion threshold. classic forms only — interview forms return 409. |
| GET | /forms/{id}/interview-analytics |
responses:read |
Breaking v1 contract: computes the current Interview V2 funnel, analysis coverage, and qualitative-code aggregates from authoritative tables. It does not read the retired legacy aggregate projection, trigger analysis jobs, or return transcripts. Classic forms return 409. |
| GET | /forms/{id}/files/{fileId} |
files:read |
Redirects (302) to a short-lived presigned URL. |
Discover workspace capabilities (kinds, render modes, block types, authoring flags)
with GET /capabilities before generating definitions.
Optimistic concurrency
draftRevisionguards definition publish/unpublish and definition PATCH.settingsSignature(sha256:digest) guards settings PATCH. It does not embed settings JSON or custom scripts.- On
409 conflict, re-read the form and re-apply the intended change. - Published
form_versionsrows are immutable. Republishing creates a new version.
Pagination
List endpoints return { "data": { "items": [...], "nextCursor": "..." | null } }.
Pass nextCursor unchanged as cursor for the next page.
Limits are capped at 100 (forms/versions default 20, responses default 50).
MCP server
form·vee exposes a stateless Streamable HTTP MCP endpoint at
https://form.vee.kr/api/mcp.
It uses the same API keys, workspace boundary, scopes, and per-key rate limit as REST.
The surface is task-oriented (five tools), not one tool per REST route.
POSTonly. Other methods return405withAllow: POSTafter auth.- No sessions, SSE resume tokens, or top-level JSON-RPC batches.
- Request body limit is 1 MiB.
- Auth profile is static Bearer API keys (no OAuth discovery endpoints).
Client setup
Claude Code:
claude mcp add --transport http formvee https://form.vee.kr/api/mcp \
--header "Authorization: Bearer fv_xxx_xxx"
Any MCP client that can set a Streamable HTTP URL and Authorization header works.
If the client sends Origin, it must exactly match the deployment origin.
Tools
Tools appear in tools/list only when the key holds the required scope.
| Tool | Scope | What it does |
|---|---|---|
forms_query |
forms:read |
Search forms, fetch one draft (with concurrency tokens), or read published version history/snapshots. |
forms_create |
forms:write (+ forms:read for copy) |
Create a blank classic/interview draft, or copy an existing readable form through the same create gates. |
forms_update |
forms:write |
Replace definition (draftRevision CAS), replace settings (settingsSignature CAS), or set a public slug. |
forms_publication |
forms:publish |
Converge publish state with expectedDraftRevision and desiredState published|draft. |
responses_query |
responses:read |
List completed responses (compact), fetch detail, or load a kind-aware aggregate report. |
Typical agent loop:
- Read
formvee://workspace/contextfor scopes, tools, and capabilities. forms_querysearch/get to obtaindraftRevisionandsettingsSignature.- Mutate with
forms_create/forms_update, thenforms_publication. - Use
responses_queryfor compact lists/reports; request transcript only when needed.
Workspace context resource
URI: formvee://workspace/context
Returns the calling key's scopes, visible tools, evaluated authoring capabilities (kinds, render modes, block types, allowlisted features), and MCP-only constraints. Agents should read this before creating definitions so they avoid disabled features.
Results & privacy
- Tool inputs/outputs use strict schemas (unknown keys rejected).
- Errors prefer
structuredContentwith{ error: { code, message, details? } }. responses_query.listis compact only (no classic answer bodies or interview transcripts).- Interview
detailkeepsincludeTranscriptdefaultfalse. - Following a classic file
downloadUrlstill requiresfiles:readon the same key.
Machine-readable contract
Prefer /api/v1/openapi.json for codegen and external
API explorers. This HTML page is the human manual for both REST and MCP. The deployment
is currently private: v1 path and OpenAPI info.version stay at
1.0.0 while contracts may still change in place.