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 project 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. Insights quotes answers back, so it needs this as well as forms:read. |
| responses:export | Export a form's responses as CSV. |
| respondents:read | Look up living respondent profiles. |
| forms:write | Create and edit a form's schema; rename, duplicate, unpublish, discard staged changes, and change the settings that live in the schema (submit copy, redirect, progress bar). |
| forms:publish | Take a form's staged changes live. |
| storage:manage | Connect and disconnect the workspace's upload destinations, and choose where a form's uploads land (including its Google Drive folder). |
| webhooks:manage | List, add, update, and remove a form's signed webhooks. |
| settings:manage | Read and patch a form's operational settings. The six presentation keys live in the schema and additionally need forms:write. |
| integrations:manage | Turn a form's Sheets, Notion, HubSpot, or Slack destination on or off, choose the account a project uses, and manage connected accounts. |
| responses:manage | Read held submissions and drafts, release held submissions, and retry or re-send deliveries. |
| members:manage | List members and invitations, change a role, and remove a member. |
| workspace:manage | Rename the workspace or project and manage sync tokens, connector tokens, code-sync policy, allowed origins, identity verification, and agent access. |
| forms:delete | Delete a form with its responses and files. |
| responses:delete | Delete a response and its uploaded files. |
| respondents:delete | Forget a respondent, optionally with every response they submitted. |
| workspace:delete | Schedule the workspace itself for deletion. |
Endpoints
A form path segment accepts a form id or slug. Any id outside the key’s project returns 404 (never 403), so the API never reveals whether an inaccessible id exists.
Everything a workspace member can do in the dashboard is here, including the outward and the irreversible actions. On this API the scope is the consent: whoever minted the key chose it. Irreversible operations ask for one thing more — a body confirm field naming the exact target: a member's email address, a token, grant, or server id, an account's exact name, a respondent's external id, or the literal identity. A value that does not match is a 409 and nothing changes, and a boolean true is rejected outright, so a bare confirmation can never stand in for a typed one.
| Method | Path | Scope | Summary |
|---|---|---|---|
| GET | /api/v1/manage/forms | forms:read | List the project's forms. |
| GET | /api/v1/manage/forms/{form} | forms:read | A form's published schema, settings, and hasDraft. ?include=draft adds the staged revision and needs forms:write. |
| PATCH | /api/v1/manage/forms/{form} | forms:write | Rename a form; old links keep working. |
| POST | /api/v1/manage/forms/{form}/unpublish | forms:write | Take a live form offline. Responses and files are kept. |
| POST | /api/v1/manage/forms/{form}/discard | forms:write | Drop a published form's staged changes; the live form stays. |
| POST | /api/v1/manage/forms/{form}/duplicate | forms:write | Copy a form into the same project as a fresh draft. A form whose deletion has started is 410. |
| GET | /api/v1/manage/forms/{form}/versions | forms:read | The form's stored schema versions. |
| GET | /api/v1/manage/forms/{form}/storage | storage:manage | Where this form's uploads land, and what they resolve to now. |
| PUT | /api/v1/manage/forms/{form}/storage | storage:manage | Pin the form's upload destination (or clear it). |
| GET | /api/v1/manage/forms/{form}/storage/folder | storage:manage | The selected Google Drive folder plus the account's writable folders. |
| PUT | /api/v1/manage/forms/{form}/storage/folder | storage:manage | Send new uploads to a Drive folder; existing files stay put. |
| DELETE | /api/v1/manage/forms/{form}/storage/folder | storage:manage | Return to the automatic per-form Drive folder. |
| 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. The six presentation keys also need forms:write. |
| 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 | Update a webhook's events or write-only receiver authentication. |
| 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. ?held=1 returns the withheld queue instead and also needs responses:manage. |
| 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. |
| POST | /api/v1/manage/forms/{form}/responses/release | responses:manage | Accept held submissions; released responses get delivered. |
| GET | /api/v1/manage/forms/{form}/drafts | responses:manage | Who is mid-fill, and what they have typed so far. |
| GET | /api/v1/manage/forms/{form}/insights | forms:read + responses:read | Totals, median completion time, the journey, and per-question analysis. The per-question breakdowns quote answers, so this costs both scopes. |
| 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/forms/{form}/deliveries | responses:manage | Per-destination delivery health and the recent outbox. Destinations are named, not spelled out: webhook target URLs stay in the dashboard. |
| POST | /api/v1/manage/forms/{form}/deliveries/retry | responses:manage | Repair failed deliveries by response, destination, row, or all. |
| POST | /api/v1/manage/forms/{form}/deliveries/redeliver | responses:manage | Send responses to every current destination again. |
| GET | /api/v1/manage/forms/{form}/integrations/{provider} | integrations:manage | Whether this form sends to google_sheets, notion, slack, or hubspot. |
| PUT | /api/v1/manage/forms/{form}/integrations/{provider} | integrations:manage | Start sending this form's responses there, or change how. |
| DELETE | /api/v1/manage/forms/{form}/integrations/{provider} | integrations:manage | Stop sending; records already created stay where they are. |
| GET | /api/v1/manage/integrations/connections | integrations:manage | The workspace's provider accounts and which one this project uses. |
| PUT | /api/v1/manage/integrations/connections/{provider} | integrations:manage | Point this project at one of those accounts. |
| DELETE | /api/v1/manage/integrations/connections/{provider} | integrations:manage | Detach this project's selected account. Other projects keep theirs. |
| DELETE | /api/v1/manage/integrations/accounts/{id} | integrations:manage + workspace:manage | Remove an account from the whole workspace. confirm = its exact name; the key acts as the person who minted it. |
| PATCH | /api/v1/manage/integrations/discord/accounts/{id} | integrations:manage | Name (or un-name) one connected Discord channel. |
| DELETE | /api/v1/manage/integrations/discord/servers/{guildId} | integrations:manage + workspace:manage | Disconnect a Discord server from the whole workspace. confirm = the server id; the key acts as the person who minted it. |
| GET | /api/v1/manage/integrations/hubspot/properties | integrations:manage | Writable HubSpot Contact properties you can map onto. |
| GET | /api/v1/manage/integrations/hubspot/pipelines | integrations:manage | HubSpot Deal pipelines and their stage ids. |
| 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. |
| DELETE | /api/v1/manage/respondents/{id} | respondents:delete | Forget a respondent. confirm = their external id. |
| PATCH | /api/v1/manage/workspace | workspace:manage | Rename the workspace. |
| PATCH | /api/v1/manage/project | workspace:manage | Rename the key's project. |
| GET | /api/v1/manage/project/code-sync | workspace:manage | The code-sync policy in force for this project. |
| PATCH | /api/v1/manage/project/code-sync | workspace:manage | Choose who may stage code-defined schemas. |
| GET | /api/v1/manage/project/origins | workspace:manage | The origins the publishable key may sync from. |
| PUT | /api/v1/manage/project/origins | workspace:manage | Replace that allow-list; an empty array means any origin. |
| GET | /api/v1/manage/project/identity | workspace:manage | Identity-verification state and how many forms depend on it. |
| POST | /api/v1/manage/project/identity | workspace:manage | Mint the identity secret; it is returned exactly once. |
| DELETE | /api/v1/manage/project/identity | workspace:manage | Turn verification off. confirm = the project slug. |
| GET | /api/v1/manage/members | members:manage | Workspace members and pending invitations. |
| POST | /api/v1/manage/members/invites | members:manage | Invite someone by email; role defaults to member. |
| DELETE | /api/v1/manage/members/invites/{id} | members:manage | Cancel a pending invitation. |
| PATCH | /api/v1/manage/members/{id} | members:manage | Change a member's role. |
| DELETE | /api/v1/manage/members/{id} | members:manage | Remove a member. confirm = their email address. |
| GET | /api/v1/manage/tokens | workspace:manage | Connector tokens on this project, metadata only. |
| DELETE | /api/v1/manage/tokens/{id} | workspace:manage | Revoke a connector token. confirm = the token id. |
| GET | /api/v1/manage/sync-tokens | workspace:manage | The project's form sync tokens. |
| POST | /api/v1/manage/sync-tokens | workspace:manage | Mint a stage-only sync token; shown exactly once. |
| DELETE | /api/v1/manage/sync-tokens/{id} | workspace:manage | Revoke a sync token. confirm = the token id. |
| GET | /api/v1/manage/agents | workspace:manage | MCP clients authorized on this project. |
| DELETE | /api/v1/manage/agents/{id} | workspace:manage | Disconnect one MCP client. confirm = the grant id. |
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. limit is 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.
Accepted responses only by default. The withheld queue is reachable through held=1, which swaps the rows rather than mixing them into a page and needs responses:manage on top of responses:read.
# 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 }Versioning and deprecation
The public contract is versioned in the URL under /api/v1. Compatible additions can ship within v1; a breaking request or response change requires a new URL version. No v1 operation is currently deprecated. If that changes, the affected response will carry the RFC 9745 Deprecation date, an RFC 8594 Sunset date when removal is scheduled, and a rel="deprecation" link to migration instructions. The changelog and OpenAPI contract will name the replacement before the sunset date.
Errors and rate limits
Errors are a stable { "error": "…" } body with the matching status. Every response advertises its quota with RateLimit-Policy; RateLimit-Limit supports clients using the earlier convention. Auth responses with current limiter state also include RateLimit, RateLimit-Remaining, RateLimit-Reset, and an authoritative Retry-After on a 429. Treat Retry-After as authoritative.
| Status | Meaning |
|---|---|
| 400 | Malformed request — a body on a GET/DELETE, an unknown key, or a missing 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 project — also returned for ids in other projects. |
| 409 | Refused in this state — an unmet precondition, or a confirm value that did not match. Nothing changed. |
| 413 | The request body exceeded the 16 KiB management cap. |
| 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 project 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.
This page for agents: /docs/api.md · index at /llms.txt
Updated