The Flow Forms API lets you read your forms and submissions and manage your groups programmatically. If you are syncing a directory, pulling submission data into another system, or building a custom integration, this is the way in.
Before writing code, check whether a webhook already does what you need. Webhooks push each submission to you as it happens; the API is for when you want to pull data on your own schedule or manage groups.
Authentication
API Tokens
API tokens belong to your account, not to an individual user. A token can read every submission in the account and subscribe a webhook to any form in the account — including private and group-restricted forms — so treat it like an administrator credential.
- Go to Admin → Account (administrators only).
- Under API Tokens, enter a token name and click Generate New Token.
- Copy the token immediately. It is shown once and cannot be retrieved later.
Send the token on every request in the Authorization header:
Authorization: Bearer YOUR_API_TOKEN
The API Tokens table shows when each token was last used, which is the quickest way to spot a token that is no longer needed. Use one token per integration so you can revoke one without breaking the others.
Base URL
Requests go to your account's own address, the same one you use to sign in:
https://YOUR-SUBDOMAIN.flowforms.app/api/v1
If your account uses a custom domain, that works too. There is no shared API hostname; a token only works against the account it was created in.
Making Your First Request
GET /me confirms your token works:
curl https://YOUR-SUBDOMAIN.flowforms.app/api/v1/me \
-H "Authorization: Bearer YOUR_API_TOKEN"
A working token returns a welcome message. Anything else returns {"message": "Unauthenticated."}.
Available Endpoints
| Method and path | What it does |
|---|---|
GET /me |
Confirms the token is valid |
GET /form |
Lists your forms |
GET /form/{id} |
One form, including its fields |
POST /submission |
Searches submissions (this is a read; the body holds your filters) |
GET /submission/{id} |
One submission, including its answers |
PUT /group |
Creates or updates a group and its members |
GET /group |
Lists groups |
GET /group/{id} |
One group, including its members |
DELETE /group/{id} |
Deletes a group |
POST /subscribe, POST /unsubscribe |
Used by the Zapier integration; you will not normally call these yourself. Subscribe is account-wide: a token may watch any form in the account, including private ones |
The API is read-only for forms and submissions. Creating forms and submissions programmatically is done through the AI Integration (MCP), not this API.
Searching Submissions
POST /submission takes your filters in the JSON body and returns a page of matching submissions, newest first.
curl -X POST https://YOUR-SUBDOMAIN.flowforms.app/api/v1/submission \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"statuses": ["pending"],
"start": "2026-01-01"
}'
Filters
Every filter is optional. With no filters, you get every submission in the account.
| Parameter | Type | What it matches |
|---|---|---|
forms |
array | Form IDs, either the for_… form or the numeric one. Filtering by a form includes its sub-forms |
statuses |
array | Any of pending, approved, denied, back (sent back), info (info requested), escalate (escalated) |
search |
string | A keyword search across submission answers, 2 characters minimum |
submitters |
array | User IDs of the people who submitted |
pending |
array | User IDs the submission is currently pending on |
participants |
array | User IDs of anyone who has acted on the submission |
terms |
array | Answers on specific fields, keyed by the field's numeric element ID: {"1234": ["Sales", "Marketing"]} |
start, end |
date | Submissions created on or after / on or before a date |
labels, sharedLabels |
array | Label IDs |
flows |
array | Flow step IDs |
sortBy, sortDirection |
string | Sort by created_at or updated_at (the default), in asc or desc order (the default is desc) |
Two things worth knowing before you rely on a filter:
- A misspelled parameter is ignored, not rejected. Send
statusinstead ofstatusesand the API quietly returns every submission in the account. If your result count looks suspiciously round, check your parameter names first. The one exception isforms: a value there that is neither afor_…ID nor a number is rejected with a422, because quietly dropping it would return every submission instead of a filtered set. termskeys are numeric element IDs, not field names.{"Department": ["Sales"]}matches nothing;{"1234": ["Sales"]}matches submissions whose element 1234 contains "Sales". Values on the same element are OR'd together; separate elements must all match.
Pagination
Pages are fixed at 20 submissions. Request later pages with a ?page= query string: POST /submission?page=2. The response carries standard links and meta blocks:
{
"data": [ ... ],
"links": {
"first": "...", "last": "...", "prev": null, "next": "...?page=2"
},
"meta": {
"current_page": 1, "last_page": 3, "per_page": 20, "total": 47,
"from": 1, "to": 20, "path": "..."
}
}
What a Submission Looks Like
{
"id": "sub_abc123",
"status": "pending",
"created_at": "2026-03-15T10:30:00.000000Z",
"updated_at": "2026-03-16T09:12:00.000000Z",
"allElements": [
{
"gid": "9d0943f9-40dc-45b9-a70c-aa09828454b2",
"name": "Employee Name",
"value": "Jane Doe",
"type": "text",
"files": [],
"updated_at": "2026-03-15T10:30:00.000000Z"
}
]
}
- Submission and form IDs are prefixed strings (
sub_…andfor_…), the same IDs you see in Flow Forms URLs. Each answer'sgidis a plain UUID identifying the form element it belongs to. - Answers arrive in
allElements, one entry per field, each with the field's name, type, and value. - File answers put the file URLs in
value(comma-separated) and full file details infiles. - Time answers carry your account's timezone abbreviation, e.g.
14:30 MST.
Fetching One Submission
When you already know which submission you want, GET /submission/{id} returns just that one:
curl https://YOUR-SUBDOMAIN.flowforms.app/api/v1/submission/sub_abc123 \
-H "Authorization: Bearer YOUR_API_TOKEN"
The ID is the sub_… string from a search result or from the submission's address in Flow Forms; the numeric ID works too. The response holds a single submission under data, in exactly the shape search returns, answers included.
An ID that does not exist — or is not an ID at all — returns 404 with {"message": "Submission not found"}. So do two kinds of submission that exist but are not addressable on their own:
- Drafts. A draft is not a finished submission yet, and the API skips drafts everywhere, search included.
- Child submissions from graduated forms. Their answers already ride along in the parent's
allElements, so fetch the parent instead.
Listing Forms
curl https://YOUR-SUBDOMAIN.flowforms.app/api/v1/form \
-H "Authorization: Bearer YOUR_API_TOKEN"
Returns your active forms (100 per page, disabled forms and sub-forms excluded) with each form's id, name, and whether it is anonymous. GET /form/{id} returns one form with its elements, which is where you find the element IDs and names the terms filter needs.
Managing Groups
Groups are the one thing the API can write, built for keeping Flow Forms in sync with an external directory.
PUT /group creates or updates a group in a single call:
curl -X PUT https://YOUR-SUBDOMAIN.flowforms.app/api/v1/group \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Nurses",
"external_id": "guid-nurses-1",
"members": [
{"email": "[email protected]", "name": "Jane Doe"},
{"email": "[email protected]", "name": "Sam Roe"}
]
}'
external_idis your system's identifier for the group. Send it and the same call updates the same group forever, even if the group is renamed. Matching without it falls back to the group name.membersreplaces the full membership when present. Members not in the list are detached; unknown email addresses become new users automatically. Leavemembersout entirely to update the group without touching membership.- The response reports what happened: whether the group was created or updated, and how many members were attached, detached, and created.
GET /group lists groups (filter to your synced ones with ?managed=1, or find one with ?external_id=). GET /group/{id} includes the member list. DELETE /group/{id} detaches all members and deletes the group, with two safety rails: the built-in "All Users" group cannot be deleted, and a group still used in a workflow is kept (members detached) with "deleted": false and the reason used_in_flows in the response.
Errors
Errors are plain JSON with a message, using standard HTTP status codes:
| Status | Body |
|---|---|
| 401 | {"message": "Unauthenticated."} — missing or invalid token |
| 404 | {"message": "Form not found"} or similar |
| 422 | {"message": "…", "errors": {"field": ["what is wrong"]}} — validation failure |
| 429 | Too many requests; see rate limiting below |
| 500 | {"message": "Server Error"} |
Rate Limiting
Requests are limited to 60 per minute for the whole account - every token in the account draws on the same budget, so two integrations running at once can throttle each other. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; when you hit the limit, the 429 response includes a Retry-After header telling you how many seconds to wait. If you are paging through a large export, pace your requests rather than firing them in a burst.
Getting Help
If a request is not doing what you expect, capture the full request and response (minus the token) and contact support from the Support section of the sidebar, or email [email protected]. The Token Last Used column under Admin → Account → API Tokens confirms whether your requests are reaching the account at all.