# Management API

Last updated: 2026-07-28

Web page: https://fillo.so/docs/api
OpenAPI contract: https://fillo.so/openapi.json
Responses and exports: https://fillo.so/docs/responses.md
Integrations: https://fillo.so/docs/integrations.md
Webhooks: https://fillo.so/docs/webhooks.md

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 full machine-readable contract is https://fillo.so/openapi.json.

<a id="auth"></a>

## Authentication
Create a key in the dashboard under *Settings → Connections → API keys*, or from the terminal with `fillo keys create`. The plaintext is shown once; Fillo stores only its hash. Send it as a bearer token on every request:

```bash
curl https://fillo.so/api/v1/manage/forms \
  -H "Authorization: Bearer fsk_your_key_here"
```

These routes are not CORS-open, so a key pasted into browser code fails fast instead of silently working. Keep `fsk_` keys on your server, out of client bundles and logs. Lost or leaked? Revoke it in settings.

<a id="scopes"></a>

## 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 (for example, `This key lacks the responses:export 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. The delete scopes (`responses:delete`, `forms:delete`, `workspace:delete`) 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. |

<a id="endpoints"></a>

## 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:

```json
{
  "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
}
```

<a id="pagination"></a>

## 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`. Withheld submissions are never returned; the API sees accepted responses only.

```bash
# 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"
```

```bash
curl -X DELETE "https://fillo.so/api/v1/manage/responses/RESPONSE_ID" \
  -H "Authorization: Bearer $FILLO_KEY"
# → { "id": "RESPONSE_ID", "deleted": true }
```

<a id="errors"></a>

## 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 `https://fillo.so/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 authenticates with OAuth 2.1 (PKCE and Dynamic Client Registration); clients discover it from `https://fillo.so/.well-known/oauth-protected-resource`. Consent is shown in product language on Fillo with the read-only scopes pre-selected; approving mints a backing workspace key that appears in your keys list as **MCP: <client>** and can be revoked there at any time. When a grant asks for per-publish approval, the publish tool returns an approval URL instead of publishing directly. Full setup, tools, and the local stdio server: https://fillo.so/docs/mcp.
