Create and edit a form
Open Settings → API Keys, create a Read and write key and copy it. Store it as FFF_API_KEY in your environment. These Bash examples use curl and jq; each step uses the variables from the previous step.
1. Check your key
export FFF_API_BASE='https://app.formformform.com/api/v1'
# Set FFF_API_KEY in your shell or agent's secret settings first.
: "${FFF_API_KEY:?Set FFF_API_KEY to your API key}"
fff() {
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $FFF_API_KEY" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' "$@"
}
fff "$FFF_API_BASE/auth/me"2. Create an inactive form and edit its questions
The blank template already includes a heading, an “Untitled field” question and a submit button. Edit the initial question and insert new questions before the button. A fields array on POST /forms is ignored.
FORM=$(fff -X POST "$FFF_API_BASE/forms" \
--data '{"title":"Contact us","template":"blank","is_active":false}')
FORM_UUID=$(printf '%s' "$FORM" | jq -r '.data.uuid')
NAME_ID=$(printf '%s' "$FORM" | jq -r '.data.fields[] | select(.type == "short_text") | .id')
# Reuse the question that the blank template already created.
fff -X PATCH "$FFF_API_BASE/forms/$FORM_UUID/fields/$NAME_ID" \
--data '{"label":"Your name","is_required":true}'
# Insert the email field before the existing submit button.
fff -X POST "$FFF_API_BASE/forms/$FORM_UUID/fields" \
--data '{"type":"email","label":"Email address","is_required":true,"position":2}'3. Review, activate and get the share link
Check the form’s settings and fields before enabling responses. Use an explicit is_active value so retrying the update does not toggle it back.
# Read back and review the complete form.
fff "$FFF_API_BASE/forms/$FORM_UUID"
# When it is ready, explicitly enable responses.
fff -X PATCH "$FFF_API_BASE/forms/$FORM_UUID" \
--data '{"is_active":true,"thank_you_message":"Thanks for getting in touch."}'
# Fetch the canonical share URL instead of guessing its host or slug.
PUBLIC_ID=$(fff "$FFF_API_BASE/forms/$FORM_UUID" | jq -r '.data.public_id')
curl --fail-with-body --silent --show-error \
-H 'Accept: application/json' \
"https://app.formformform.com/api/f/$PUBLIC_ID" | jq -r '.public_url'To edit this form later, save FORM_UUID. Fetch the form again before editing to get its current field IDs, UUIDs, settings and order.
Authentication & API keys
Create named keys in Settings → API Keys. The secret is shown once. The list shows when each key was created and last used; revoke a key there to stop its access immediately. Keys have no automatic expiry.
- Read only: list and read forms, fields, templates and responses; export responses.
- Read and write: all reads, plus create, edit, duplicate, activate and delete forms and fields, and delete responses.
Keys apply to the owner’s account, not a single form. They cannot change account settings, passwords or billing, and they do not authenticate the OAuth connector API.
Authorization: Bearer YOUR_API_KEY
Accept: application/json
Content-Type: application/jsonSend Accept: application/json on every call and Content-Type: application/json for JSON bodies. Without the Accept header, some failures can return an HTML redirect instead of JSON. Keep keys in server-side or agent secret storage, outside public form code.
Alternative: sign in for a token
POST /auth/login accepts email, password and an optional device_name. It returns token, token_type and user. Login tokens have broad account permissions; use a settings-issued key for a form integration.
If two-factor authentication is enabled, login returns two_factor: true and challenge_token. Send that token with code or recovery_code to POST /auth/two-factor-challenge. Pass device_name again to label the issued token. A challenge lasts five minutes and is consumed even on a failed attempt; repeat login before trying another code.
GET /auth/me checks the credential. POST /auth/logout revokes only the credential making the request and returns 204.
Identifiers & response shapes
- Form management:
uuid - Use
data.uuidin/forms/{form}. The numeric formidis not the management route key. - Field management: numeric
id - Use
data.fields[].idto edit or delete a field and in reorder’sidsarray. - Conditional logic: field
uuid - Use
data.fields[].uuidinsidelogic.conditions[].field_uuid. - Public forms:
public_id - The short share identifier selects a public form. Public field
idvalues are UUID strings, unlike numeric field IDs in the owner API. Use the returned UUIDs as public answer keys. - Responses: submission
uuid - Use the submission UUID in
/submissions/{submission}.
Form and field responses wrap the resource in data. Lists use data, links and meta. Login, reorder, public submission receipts and the legacy /api/f surface have their own shapes. Never assume every response contains data.
This is an abbreviated owner form response; the OpenAPI document describes the returned properties.
{
"data": {
"uuid": "11111111-1111-4111-8111-111111111111",
"public_id": "AbCd1234",
"title": "Contact us",
"slug": "contact-us",
"is_active": false,
"fields": [
{
"id": 102,
"uuid": "22222222-2222-4222-8222-222222222222",
"type": "short_text",
"label": "Your name",
"is_required": true,
"position": 1
}
]
}
}Pagination uses ?page=2&per_page=25; forms and responses default to 25 and support up to 100 items per page. Follow links.next until it is null. Templates return categories, each with its own templates array.
Endpoint reference
All paths below are relative to https://app.formformform.com/api/v1. Management routes require a bearer token. Public form routes and login do not. Successful creates return 201, reads and updates 200, and deletes 204 with no JSON body.
Sign in for a token
POST /auth/login
Optional alternative to creating a key in Settings. Login tokens have broad account access, unlike scoped API keys. Wrong credentials return 422. After five failed passwords per email and IP, requests are throttled for up to a minute. A two-factor account returns a challenge instead of a token.
Success: 200 · Body: Login
{
"email": "you@example.com",
"password": "YOUR_PASSWORD",
"device_name": "my-integration"
}Complete two-factor sign-in
POST /auth/two-factor-challenge
The challenge expires after five minutes and is consumed on every attempt, including failed attempts. Sign in again for a fresh challenge before retrying. Pass device_name here too if you want a custom label.
Success: 200 · Body: TwoFactorChallenge
{
"challenge_token": "CHALLENGE_FROM_LOGIN",
"code": "123456",
"device_name": "my-integration"
}Check your authentication
GET /auth/me
Available to read-only and read and write API keys.
Success: 200
Revoke the current token
POST /auth/logout
Immediately revokes only the key or token used for this request. Available to read-only and read and write API keys.
Success: 204
List your forms
GET /forms
Newest first. Includes field counts and plan metadata; use GET /forms/{form} for field definitions. Pagination defaults to 25, capped at 100. Available to read-only and read and write API keys.
Success: 200
Create a form
POST /forms
Send settings and an optional template slug. The blank template seeds a heading, one short_text field and a submit_button. Custom fields arrays are ignored here. Use is_active: false while assembling the form. Requires a read and write API key.
Success: 201 · Body: FormCreate
{
"title": "Contact us",
"template": "blank",
"is_active": false,
"thank_you_message": "Thanks — we will be in touch."
}Read a form and its fields
GET /forms/{form}
Available to read-only and read and write API keys.
Success: 200
Update form settings
PATCH /forms/{form}
Updates only supplied settings. Renaming the form changes its slug but does not change the heading field label; edit that field separately. Set is_active explicitly for repeatable activation. Requires a read and write API key.
Success: 200 · Body: FormUpdate
{
"title": "Contact our team",
"is_active": true
}Delete a form
DELETE /forms/{form}
Permanently deletes the form, its responses and associated stored files. There is no confirmation parameter on v1. Requires a read and write API key.
Success: 204
Duplicate a form
POST /forms/{form}/duplicate
Copies fields and supported settings, including active status and encryption configuration; does not copy submissions. Requires a title. Requires a read and write API key.
Success: 201 · Body: DuplicateForm
{
"title": "Contact us — copy"
}Toggle whether a form accepts responses
PATCH /forms/{form}/toggle-active
Flips the current value. Prefer PATCH /forms/{form} with an explicit is_active value in retryable automation. Requires a read and write API key.
Success: 200
List available form templates
GET /templates
Returns categories in data, each containing a templates array. Use a template’s id (its slug) in POST /forms. Available to read-only and read and write API keys.
Success: 200
Add a field
POST /forms/{form}/fields
Creates one field. position inserts at a zero-based index, shifting later fields. Omit it to append; insert before the existing submit button for a normal question. Requires a read and write API key.
Success: 201 · Body: FieldCreate
{
"type": "email",
"label": "Email address",
"is_required": true,
"position": 2
}Edit a field
PATCH /forms/{form}/fields/{field}
Uses the numeric field id from the owner API. Updates supplied properties. Read the existing field first and preserve the parts of settings and logic you want to keep. Requires a read and write API key.
Success: 200 · Body: FieldUpdate
{
"label": "Your email",
"is_required": true,
"placeholder": "you@example.com"
}Delete a field
DELETE /forms/{form}/fields/{field}
Permanently deletes a field and its associated stored files. The endpoint does not preserve the heading or submit button for you; keep both when editing. Requires a read and write API key.
Success: 204
Reorder fields
POST /forms/{form}/fields/reorder
Use numeric IDs from a fresh GET of the form. Send every field once, with the heading first and submit button last. IDs are not UUIDs. Requires a read and write API key.
Success: 200 · Body: Reorder
{
"ids": [
101,
102,
104,
103
]
}List responses
GET /forms/{form}/submissions
Newest first; paginated, with today_count alongside data. Match values by field_uuid. Encrypted payload is only available on the detail endpoint. Available to read-only and read and write API keys.
Success: 200
Export responses as CSV
GET /forms/{form}/submissions/export
Returns CSV bytes, not a JSON envelope. Columns are input fields, Submitted At, IP Address, Filled At and Was Offline. Encrypted answers cannot be decrypted by the server. Available to read-only and read and write API keys.
Success: 200
Read one response
GET /forms/{form}/submissions/{submission}
Submission is a UUID. Includes encrypted_payload on encrypted forms, and prev/next UUIDs when neighbours exist. Available to read-only and read and write API keys.
Success: 200
Delete a response
DELETE /forms/{form}/submissions/{submission}
Permanently removes the response. Requires a read and write API key.
Success: 204
Read an active form without authentication
GET /public/forms/{public_id}
Public field id values are UUID strings. This request increments the form view count. Inactive forms return 404.
Success: 200
Submit answers without authentication
POST /public/forms/{public_id}/submissions
Uses field UUIDs as fields keys. Uploaded files named by upload_uuids are attached on both public submission APIs. Paid forms require online checkout and return 409 here.
Success: 201 · Body: PublicSubmission
{
"fields": {
"11111111-1111-4111-8111-111111111111": "you@example.com"
}
}Field types
Create fields one at a time. dropdown, radio and checkbox require an options array of strings. A checkbox answer is an array of selected option strings.
| Type | Purpose | Settings |
|---|---|---|
short_text | Single-line text | max_length, field_width |
long_text | Multi-line text | rows, max_length, field_width |
email | Email address | max_length, field_width |
number | Numeric answer | min, max, step, field_width |
phone | Phone number | max_length, field_width |
date | Date | min_date, max_date, field_width |
dropdown | One selected option; options required | field_width |
radio | One selected option; options required | field_width |
checkbox | Multiple selected options; options required | field_width |
file | File upload | accept, max_size, field_width |
heading | Opening heading or section title | — |
paragraph | Rich text in content; label optional | — |
divider | Separator; label optional | — |
image | Image URL in content; label optional | alt, width, image_uuid |
hidden | Hidden value, set default_value | — |
page_break | Start another page; label optional | — |
payment | Products and checkout; configure in editor | — |
submit_button | Submit action; keep this last | button_alignment |
Use content for paragraph HTML or an image URL. Paragraph HTML is sanitised. Keep a heading and a submit button on the form: the v1 deletion endpoint permits removing them. The payment field alone does not configure checkout; use the editor to set up payment providers and products.
Request properties
PATCH requests update supplied properties. Omitted properties keep their current values. Send null only where allowed. Unknown properties are generally ignored rather than rejected, so always read the result back.
FormCreate
Create a form seeded from a template. fields is not accepted here; add or edit fields through the field endpoints. Use is_active: false while building.
titlestring · required- Maximum 255 characters.
descriptionstring or null- Maximum 2000 characters.
thank_you_messagestring or null- Maximum 2000 characters.
redirect_urlstring or null- HTTP or HTTPS URL. Maximum 500 characters. Format: uri.
notify_on_submissionboolean or null- Defaults to true.
is_activeboolean or null- Defaults to true. Send false while building.
templatestring- Template slug returned as templates[].id by GET /templates. Default blank; an unknown slug falls back to blank. Maximum 100 characters.
FormUpdate
Partial settings update. fields, slug and public_id are ignored. Encryption configuration freezes after the first submission; locked encryption changes are ignored. Payment forms cannot enable encryption.
titlestring- Maximum 255 characters.
descriptionstring or null- Maximum 2000 characters.
thank_you_messagestring or null- Maximum 2000 characters.
redirect_urlstring or null- HTTP or HTTPS URL. Maximum 500 characters. Format: uri.
notify_on_submissionboolean or null- Optional.
is_activeboolean or null- Optional.
show_branded_footerboolean- Only paid accounts can remove branding; free accounts are forced to true.
is_encryptedboolean or null- Optional.
encryption_public_keystring or null- Armoured PGP PUBLIC KEY BLOCK; required when enabling encryption unless one is already stored. Maximum 20000 characters.
encryption_key_fingerprintstring or null- Maximum 255 characters.
FieldCreate
One field per request. label is required except for paragraph, divider, image and page_break. options is required for dropdown, radio and checkbox. payment is a supported type but checkout configuration requires the editor.
typestring · required- Values: short_text, long_text, email, number, phone, date, dropdown, radio, checkbox, file, heading, paragraph, divider, image, hidden, page_break, payment, submit_button.
labelstring or null- Required on creation except paragraph, divider, image and page_break. Maximum 255 characters.
contentstring or null- Paragraph HTML or image URL, according to type. Maximum 10000 characters.
descriptionstring or null- Maximum 500 characters.
placeholderstring or null- Maximum 255 characters.
optionsstring[] or null- Optional.
default_valuestring or null- Maximum 255 characters.
settingsFieldSettings- See FieldSettings in this reference and the OpenAPI document.
logicFieldLogic- See FieldLogic in this reference and the OpenAPI document.
is_requiredboolean or null- Optional.
positioninteger or null- Zero-based insertion position; omitted or null appends. Minimum 0.
FieldUpdate
Partial field update. position and uuid are ignored; use reorder to move fields. When changing type, include a compatible label and options. settings and logic replace their stored objects; send the whole desired object.
typestring- Values: short_text, long_text, email, number, phone, date, dropdown, radio, checkbox, file, heading, paragraph, divider, image, hidden, page_break, payment, submit_button.
labelstring or null- Required on creation except paragraph, divider, image and page_break. Maximum 255 characters.
contentstring or null- Paragraph HTML or image URL, according to type. Maximum 10000 characters.
descriptionstring or null- Maximum 500 characters.
placeholderstring or null- Maximum 255 characters.
optionsstring[] or null- Optional.
default_valuestring or null- Maximum 255 characters.
settingsFieldSettings- See FieldSettings in this reference and the OpenAPI document.
logicFieldLogic- See FieldLogic in this reference and the OpenAPI document.
is_requiredboolean- Optional.
FieldSettings
minnumber or null- Optional.
maxnumber or null- Optional.
stepnumber or null- Minimum 0.
max_lengthinteger or null- Minimum 1.
rowsinteger or null- Minimum 1. Maximum 50.
min_datestring or null- Format: date.
max_datestring or null- Format: date.
field_widthstring or null- Values: small, medium, large, full.
button_alignmentstring or null- Values: left, center, right.
acceptstring or null- Maximum 255 characters.
max_sizeinteger or null- Field metadata in KB. Actual uploads have a separate server-enforced 2 MB limit. Minimum 1. Maximum 51200.
altstring or null- Maximum 255 characters.
widthinteger or null- Minimum 1.
image_uuidstring or null- Maximum 36 characters.
FieldLogic
Send null to remove logic. Configure after creating the referenced fields.
actionstring- Values: show, require.
condition_typestring- Values: all, any.
conditionsobject[]- Optional.
Reorder
Send all numeric field IDs from this form once, in the desired order. This endpoint does not require a complete list; a partial list can leave ambiguous positions.
idsinteger[] · required- Optional.
DuplicateForm
titlestring · required- Maximum 255 characters.
PublicSubmission
For ordinary forms use fields. Encrypted forms require encrypted_payload and prohibit fields. Payment forms return 409 on this endpoint. client_uuid enables deduplication of offline retries.
fieldsobject- Answers keyed by the field UUID returned by the public form. Checkbox answers are arrays of option strings.
encrypted_payloadstring- Armoured PGP message for encrypted forms; omit fields.
upload_uuidsstring[]- Optional.
client_uuidstring- Format: uuid.
filled_atstring- Format: date-time.
was_offlineboolean- Optional.
Encryption: only send the public key and its fingerprint to the API. Encryption settings freeze after the first submission; locked changes are silently ignored. Enabling encryption on a payment form returns a validation error. The server cannot decrypt encrypted answers.
Branding: show_branded_footer: false requires a paid account. A free account is forced back to true.
Conditional logic
Create the controlling question first, then fetch its UUID. PATCH the dependent field using its numeric ID. For example, show “Company name” when the controlling question equals “Business”:
{
"label": "Company name",
"logic": {
"action": "show",
"condition_type": "all",
"conditions": [
{
"field_uuid": "11111111-1111-4111-8111-111111111111",
"operator": "equals",
"value": "Business"
}
]
}
}Replace the example field_uuid with the controlling field’s actual UUID on this form. Actions are show and require; condition types are all and any.
Operators: equals, not_equals, contains, not_contains, greater_than, less_than, is_empty and is_not_empty. Comparison values are strings. Set logic: null to remove the rule. When changing logic or settings, send the whole desired object: these objects are replaced, not deeply merged.
Responses & public submissions
Owner response endpoints require a key. Match each item in values by field_uuid; empty or conditionally hidden fields may have no entry. Encrypted forms expose ciphertext on the single-response endpoint, not plaintext answers. CSV export returns a file rather than a JSON envelope.
For a custom form renderer, read GET /public/forms/{public_id} and post answers to POST /public/forms/{public_id}/submissions, with no API key. Put field UUIDs from the public response in the fields object. The public API’s field id is also that UUID.
client_uuid identifies a submission for deduplicating offline retries. filled_at is the time it was filled and was_offline records offline collection. Do not assume form creation or other writes support idempotency keys.
Uploads and existing public forms
The separate GET https://app.formformform.com/api/f/{public_id} endpoint returns an unwrapped form document with public_url, encryption metadata and payment_required. A plain form can receive answers via POST at the same URL; it is limited to 30 submissions per minute per IP. For paid forms, use the hosted form’s checkout flow.
Upload a file before submitting using multipart POST /f/{public_id}/uploads on the app host. Send the file as file and the upload field’s UUID as form_field_id. Put the returned upload UUID in that field’s answer and in upload_uuids. Both public submission routes attach uploaded files. The server limit is 2 MB per file, with up to 50 upload references per submission. Encrypted uploads require the client-side encryption flow.
Downloading or previewing submitted files as the owner requires a browser session. The management API returns upload identifiers, not bearer-authenticated download URLs.
Errors & retries
401— authentication required- The key is missing, invalid or revoked. Check the Bearer header.
403— access denied- The key lacks write access, the form belongs to someone else, or account restrictions apply. Settings-issued keys cannot access account or billing endpoints.
404— not found- Check the form UUID, field ID, submission UUID or public ID. Public inactive forms return 404.
409— conflicting operation- The v1 public submission endpoint refuses payment forms; use online checkout.
422— validation failed- Inspect
errors, correct the payload and try again. Wrong login credentials also return 422. 429— rate limited- Respect
Retry-Afterwhen present and back off. Login limits failed passwords per email and IP; key creation is limited to 10 requests per minute.
{
"message": "The title field is required.",
"errors": {
"title": [
"The title field is required."
]
}
}The v1 form-management routes do not currently advertise a general per-minute quota. Use bounded concurrency and backoff. After an uncertain create response, list and inspect forms before retrying; blindly retrying POST can create duplicates. Activation via explicit is_active is safer to retry than the toggle endpoint.
Instructions for AI agents
Give your agent the Markdown reference or OpenAPI schema and supply the key through its secret configuration. This prompt is a starting point:
Read https://www.formformform.com/api.md.
Use the API key in FFF_API_KEY with the /api/v1 base URL.
Create an inactive form for [describe the form].
Reuse or edit the blank template's initial fields.
Add the remaining questions before the submit button.
Use returned numeric field IDs for edits and UUIDs for logic.
Fetch the form again and check every label, option, rule and position.
Show me the result before activating it.
After activation, fetch /api/f/{public_id} for the canonical public_url.For edits, start with GET /forms/{form}, preserve existing answers and field identities where possible, and update only what the user requested. Deleting and recreating a field changes its identity. Ask the user before deleting forms or responses.
OAuth connectors are a separate API
The /api/mcp REST surface serves OAuth-based AI connectors and supports batch form creation and field edits. It uses different tokens, scopes, identifiers and envelopes; a settings-issued API key is for /api/v1.
The connector’s contract describes its capabilities. Its GPT Actions schema deliberately omits editing operations. Use this reference’s OpenAPI schema for an agent using an API key to create and edit forms.
Coverage & limitations
This reference covers authentication, forms, fields, templates, responses and basic public submission endpoints. It documents the existing API rather than promising parity with every editor setting.
- Custom forms are assembled through field calls; v1 has no bulk
fieldscreate or replace request. - Webhook configuration, payment providers/products and uploaded editor images are managed through the editor.
- Settings-issued keys cannot manage account settings, passwords or store billing.
- Form slug and public ID are server-managed. Changing a form’s title does not rename its heading field.
- Read responses after writes: unsupported attributes and some plan-locked settings can be ignored or overridden.
For the shorter walkthrough, see Using the REST API. For submission delivery, see webhooks.