Feature guides
How the newer parts of the platform work through the API. Scopes are the ones the request needs.
Results triage
Give every response a status (new, in_review, approved, rejected, done), an assignee and tags without touching its answers. GET /forms/{id}/triage (submission:read) returns the state of triaged responses and who can be assigned; PATCH /submissions/{id}/triage and POST /submissions/bulk-triage (up to 500, form:write) change it. Corrections to an answer are separate: POST /submissions/{id}/edits keeps the original and .../edits/{editId}/revert brings it back. Saved views of filters are under /forms/{id}/views.
Spam and captcha
POST /submissions/{id}/spam files a response as spam; POST /submissions/{id}/restore moves it back and fires the completion events, webhooks and notifications at that moment. settings.minCompletionMs files implausibly fast completions as spam automatically. A workspace stores its own Turnstile or hCaptcha keys with PUT /captcha (workspace:manage, secret write-only); the deployment-wide CAPTCHA_* variables are the fallback, and the form turns it on with settings.captcha.
Share links for results
POST /forms/{id}/share-links (analytics:read) makes a signed, revocable, read-only link to a results page. Mode aggregate shows charts only; mode respondents also lists individual answers for the fieldIds you choose and needs submission:export. Sensitive, file and signature questions are refused. The url is a bearer capability: set expiresInDays and a password, and revoke with DELETE /share-links/{linkId}.
Version history and restore
GET /forms/{id}/version-history # versions, draft changes, undoable restores
GET /forms/{id}/versions/compare?from=3&to=draft
POST /forms/{id}/versions/3/restore # replaces the DRAFT; the old draft is snapshotted first
POST /forms/{id}/restores/{snapshotId}/undo # once per snapshotA restore never changes the live form. Review the draft, then publish. Needs form:write.
Invitations
POST /forms/{id}/invitations creates up to 500 links, each reusable or single-use (singleUse), optionally emailed to the address you give. Attributes you attach are available to logic as the respondent condition source. Each token and link appears once in the response; only a hash is stored. Revoke with DELETE /forms/{id}/invitations/{invitationId}.
Respondent edit links
With settings.respondentEdit.enabled the respondent gets a signed link (valid expiresDays, default 30) to change their answers. GET and PUT /public/forms/{slug}/edit serve it. Validation is the same as a first submit and every change stays in the response's edit history.
Email and phone verification
Set verify: true on an email or phone field. The form calls POST /public/forms/{slug}/verify with action: send and then check; codes last 10 minutes and allow 5 attempts. The proof from check goes back with the submission. SMS needs SMS_PROVIDER=twilio with Twilio credentials and has not been verified against Twilio.
Input masks, recording and address lookup
File uploads accept up to 250 MB per file on local disk storage. With S3 multipart storage configured (S3_BUCKET and keys) maxSizeMb can be set up to 5120 (5 GB): the browser uploads in 8 MB parts straight to the bucket. Files above 256 MB are not loaded into memory, so they are not re-encoded or hashed, and they are refused when a virus scanner is configured.
mask on short_text and phone uses 9 for a digit, a for a letter and * for either. record on file_upload lets people record video or audio in the browser. autocomplete on address proxies suggestions through ADDRESS_PROVIDER (google or mapbox) so the key never reaches the browser; without a provider the form keeps its manual fields.
Coupons, quotes and checkout recovery
POST /public/forms/{slug}/coupon answers only { valid }. POST .../quote returns the server-side order summary. After a provider redirect, poll GET /public/payments/{submissionId}; a lapsed checkout is recovered with POST .../checkout, which never creates a second charge for a paid order.
Data sources
POST /data-sources (integration:manage) defines a server-side lookup against Airtable, Google Sheets or Notion. Choice fields can load their options from it (GET /public/forms/{slug}/choices/{fieldId}, with origin telling live, cache, stale, fallback or unavailable), and a key lookup can prefill from one record. Only columns you expose are returned.
Workspace tables
/tables holds data that lives in the workspace and not in a form: typed columns (text, number, date, select, checkbox, link), rows edited in a grid at /tables, POST /tables/{id}/import and GET .../export for CSV, and /tables/{id}/rows for create, search, update and delete (reads need form:read, writes form:write). Forms read from a table through POST /data-sources with tableId in place of a connection: kind: "choices" fills a choice field (labelField and valueField are column ids) and kind: "record" prefills from the row whose matchField column equals the key in the link. A form writes to a table with settings.tableWrite (tableId, mode append or upsert, keyColumn, and map of column id to field id or $id): every completed response adds a row, or updates the row with the same key. A column keeps its type for life; a value a column rejects is skipped and never fails the response. The SDK has library.tables; MCP has list_tables, add_table_rows, import_table_csv and the rest.
Person picker
The person field stores { id, name }. source: "members" lists people in the organization, and only on a form whose audience is set to organization members (PUT /forms/{id}/access mode members), so a public form never lists staff. source: "table" lists the rows of tableId by labelColumn. GET /public/forms/{slug}/people/{fieldId}?q= searches; on submit the server checks the id again and replaces the name with the real one.
Country and IP rules
PUT /forms/{id}/access accepts countryAllow and countryDeny (two-letter codes) beside ipAllow and ipDeny, in every audience mode. The country is the one the host or CDN reports (x-vercel-ip-country or cf-ipcountry, the same source as system.country). Deny wins; with an allow list an unknown country is blocked. A blocked visitor gets a closed message on the page and a 403 from the API.
Logic test scenarios
settings.logicTests holds named scenarios: answers to feed the engine and what must come out (ending, visible, hidden fields, and the path of pages in order). Save one from the editor's Simulator. POST /forms/{id}/logic-tests/run runs them on the draft, and publishing runs them too: with gate: "block" a failure stops the publish (422 with the failing scenarios), with "warn" it is added to the publish warnings.
Filling a form by text message
With settings.sms.enabled a form can be filled over SMS or WhatsApp, one question per message. Point a Twilio number's incoming-message webhook at /api/webhooks-inbound/sms (add ?form=<link name> to give the number to one form; otherwise the first word a person texts is the link name). Requests need a valid X-Twilio-Signature (TWILIO_AUTH_TOKEN). Supported: text, email, phone, link, number, rating, yes or no, date and choice by number; SKIP passes an optional question and STOP opts out. Conversations are kept per phone number (stored as a salted hash) and dropped after expiryMinutes of silence; the end is a normal submission through the same validation. Forms with a password, CAPTCHA, audience rules or questions that cannot be answered by text are not offered. Not verified against a live Twilio account.
Microsoft Excel and SharePoint
The Microsoft 365 integration adds excel_create_workbook (builds an .xlsx with a table whose headers you name and uploads it to OneDrive or a SharePoint library; returns the item id), excel_add_row and excel_add_rows (up to 500 rows for a backfill). driveId targets a SharePoint document library. Not verified against a live tenant.
Approvals and e-signature
Workflows can pause for approval. GET /approvals lists them, approvers decide in the app or from a personal one-time emailed link, and resend and cancel need integration:manage. POST /signing sends a document to up to 10 signers with a hash-chained audit trail that GET /signing/{id}/verify recomputes. FormFlow makes no compliance or certification claim for signatures; whether they are legally sufficient depends on your jurisdiction and use.
Integration health
GET /integrations/health shows each connection's last success, failing step and retry state. POST /integrations/connections/{id}/test runs a read-only check (providers that cannot be checked without sending data answer unchecked). POST /integrations/executions/{id}/replay reruns the failed steps once per failure state and warns about steps whose target cannot de-duplicate.
Targeting and experiments
GET /public/targeting?workspace=... serves the enabled trigger rules and audiences of published forms. It contains no answers. A/B tests (patch or whole-form versions, multivariate, question-level) are created in the app because a version can carry custom code, and are also available through the API, the SDK and MCP: list and read results, start, stop, declare a winner, and report downstream conversions with POST /experiments/{id}/conversions (form:write). An experiment verdict says when the sample is too small; do not call a winner from it. See A/B testing onboarding with PostHog.
New theme and field options
| Where | Option |
|---|---|
| theme | questionSize xs (16-20px titles), choiceStyle tile, customFonts, logoPlacement top_right, background fit, position and imageOpacity |
| image_choice | cardStyle tile, cardSize, badgePlacement |
| media | frame, fit, ratio, scrim, textTone, size, vignette |
| welcome | hideTitle, inlineFirst, align, mediaPosition, lockup |
| settings | navStyle, sidePanel, header, i18n, offline, kiosk, softCap, blocks, quotas, notifications.rules |
| logic | set_value, disable_field, enable_field actions; lead_score and score condition sources |