Management API
List and manage your forms, responses, and respondents from your own backend. The API lives under /api/v1/manage, authenticates with a scoped workspace secret key, and returns keyset-paginated JSON. The complete contract is available as openapi.json.
Authentication
Create a key under Settings → Connections → API keys. The plaintext is shown once; Fillo stores only its hash. Send it as a bearer token on every request:
curl https://fillo.so/api/v1/manage/forms \
-H "Authorization: Bearer fsk_your_key_here"Scopes
Every key stores an explicit set of scopes. A request for a scope the key lacks returns 403 with a stable message that names the missing scope. Mint keys in the dashboard or with fillo keys create --preset read|agent|full: read is the three read scopes, agent adds forms:write, forms:publish, and responses:export, and full is every scope except the irreversible delete tier. Delete scopes are never part of a preset — name them explicitly to grant them.
| Scope | Grants |
|---|---|
| forms:read | List forms and versions; read a form's published schema and settings. |
| responses:read | List, read, and summarize responses, including one respondent's history. |
| responses:export | Export a form's responses as CSV. |
| respondents:read | Look up living respondent profiles. |
| webhooks:manage | List, add, update, and remove a form's signed webhooks. |
| settings:manage | Read and patch a form's operational settings. |
| responses:delete | Delete a response and its uploaded files. |
Endpoints
A form path segment accepts a form id or slug. Any id outside the key’s workspace returns 404 (never 403), so the API never reveals whether an inaccessible id exists.
| Method | Path | Scope | Summary |
|---|---|---|---|
| GET | /api/v1/manage/forms | forms:read | List the workspace's forms. |
| GET | /api/v1/manage/forms/{form} | forms:read | A form's published schema, settings, and hasDraft. |
| GET | /api/v1/manage/forms/{form}/versions | forms:read | The form's stored schema versions. |
| GET | /api/v1/manage/forms/{form}/settings | settings:manage | The form's operational settings. |
| PATCH | /api/v1/manage/forms/{form}/settings | settings:manage | Patch one or more operational settings. |
| GET | /api/v1/manage/forms/{form}/webhooks | webhooks:manage | List the form's webhooks (never the secret). |
| POST | /api/v1/manage/forms/{form}/webhooks | webhooks:manage | Add a webhook; the signing secret is returned once. |
| PATCH | /api/v1/manage/forms/{form}/webhooks/{id} | webhooks:manage | Toggle a webhook's abandoned-draft delivery. |
| DELETE | /api/v1/manage/forms/{form}/webhooks/{id} | webhooks:manage | Delete a webhook. |
| GET | /api/v1/manage/forms/{form}/responses | responses:read | A keyset page of the form's responses. |
| GET | /api/v1/manage/forms/{form}/responses/export | responses:export | Stream the form's responses as a CSV download. |
| GET | /api/v1/manage/forms/{form}/responses/summary | responses:read | Totals, per-field answer rates, and choice distributions. |
| GET | /api/v1/manage/responses/{id} | responses:read | One response with its file references. |
| DELETE | /api/v1/manage/responses/{id} | responses:delete | Delete a response and its files. |
| GET | /api/v1/manage/respondents | respondents:read | Look up respondents by externalId or email. |
| GET | /api/v1/manage/respondents/{id}/responses | respondents:read + responses:read | One person's response history. |
A response row looks like this:
{
"id": "resp_…",
"formId": "form_…",
"data": { "fld_score": 9 },
"meta": { "source": "app.example.com/feedback", "respondent": { "id": "user_42", "verified": true } },
"formVersionId": "ver_…",
"createdAt": "2026-07-13T10:00:00.000Z",
"updatedAt": null
}Pagination and filters
List endpoints return { data, nextCursor }. Pass nextCursor back as cursor for the next page; a null cursor means the last page. limitis 1–100 (default 50). Response filters use the same grammar as the responses grid and CSV export — range, q, source, respondent, and a repeatable where=fieldId:op:value. Withheld submissions are never returned; the API sees accepted responses only.
# Filter + walk pages. 'where' is repeatable as fieldId:op:value.
curl -G "https://fillo.so/api/v1/manage/forms/FORM_ID/responses" \
-H "Authorization: Bearer $FILLO_KEY" \
--data-urlencode "range=30d" \
--data-urlencode "where=fld_score:gt:8" \
--data-urlencode "limit=100"
# → { "data": [ … ], "nextCursor": "resp_abc" }
# Next page: add --data-urlencode "cursor=resp_abc"curl -X DELETE "https://fillo.so/api/v1/manage/responses/RESPONSE_ID" \
-H "Authorization: Bearer $FILLO_KEY"
# → { "id": "RESPONSE_ID", "deleted": true }Errors and rate limits
Errors are a stable { "error": "…" } body with the matching status. Rate-limit responses carry a Retry-After header in seconds.
| Status | Meaning |
|---|---|
| 400 | Malformed request — a body on a GET/DELETE, or a missing required parameter. |
| 401 | Missing, malformed, unknown, revoked, or expired key. |
| 403 | The key is valid but lacks the required scope. |
| 404 | No such resource in this workspace — also returned for ids in other workspaces. |
| 429 | Per-key (120/min) or per-workspace (240/min) rate limit; honor Retry-After. |
Remote MCP
The same scopes back Fillo’s remote MCP server at /api/mcp (Streamable HTTP, stateless): connect a hosted MCP client to run forms and read responses over OAuth instead of pasting a bearer key. It uses OAuth 2.1 with PKCE and Dynamic Client Registration, discoverable at /.well-known/oauth-protected-resource. Consent is shown in product language with the read-only scopes pre-selected; approving mints a backing workspace key that appears in your keys list as MCP: <client> and is revocable there. When a grant asks for per-publish approval, the publish tool returns an approval URL instead of publishing directly. See the MCP server docs for setup, tools, and the local stdio server.
Updated