Flow Forms Docs Flow Forms Docs

API Getting Started

On this page 15

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.

  1. Go to Admin → Account (administrators only).
  2. Under API Tokens, enter a token name and click Generate New Token.
  3. Copy the token immediately. It is shown once and cannot be retrieved later.

Send the token on every request in the Authorization header:

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

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

bash
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.

bash
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 status instead of statuses and the API quietly returns every submission in the account. If your result count looks suspiciously round, check your parameter names first. The one exception is forms: a value there that is neither a for_… ID nor a number is rejected with a 422, because quietly dropping it would return every submission instead of a filtered set.
  • terms keys 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:

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

json
{
  "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_… and for_…), the same IDs you see in Flow Forms URLs. Each answer's gid is 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 in files.
  • 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:

bash
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

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

bash
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_id is 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.
  • members replaces the full membership when present. Members not in the list are detached; unknown email addresses become new users automatically. Leave members out 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.