For developers & AI agents

Forms API reference

Create forms, edit their questions and settings, add conditional logic, and read responses from your own code or AI agent.

Base URL: https://app.formformform.com/api/v1

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/json

Send 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.uuid in /forms/{form}. The numeric form id is not the management route key.
Field management: numeric id
Use data.fields[].id to edit or delete a field and in reorder’s ids array.
Conditional logic: field uuid
Use data.fields[].uuid inside logic.conditions[].field_uuid.
Public forms: public_id
The short share identifier selects a public form. Public field id values 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.

TypePurposeSettings
short_textSingle-line textmax_length, field_width
long_textMulti-line textrows, max_length, field_width
emailEmail addressmax_length, field_width
numberNumeric answermin, max, step, field_width
phonePhone numbermax_length, field_width
dateDatemin_date, max_date, field_width
dropdownOne selected option; options requiredfield_width
radioOne selected option; options requiredfield_width
checkboxMultiple selected options; options requiredfield_width
fileFile uploadaccept, max_size, field_width
headingOpening heading or section title—
paragraphRich text in content; label optional—
dividerSeparator; label optional—
imageImage URL in content; label optionalalt, width, image_uuid
hiddenHidden value, set default_value—
page_breakStart another page; label optional—
paymentProducts and checkout; configure in editor—
submit_buttonSubmit action; keep this lastbutton_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.

title string · required
Maximum 255 characters.
description string or null
Maximum 2000 characters.
thank_you_message string or null
Maximum 2000 characters.
redirect_url string or null
HTTP or HTTPS URL. Maximum 500 characters. Format: uri.
notify_on_submission boolean or null
Defaults to true.
is_active boolean or null
Defaults to true. Send false while building.
template string
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.

title string
Maximum 255 characters.
description string or null
Maximum 2000 characters.
thank_you_message string or null
Maximum 2000 characters.
redirect_url string or null
HTTP or HTTPS URL. Maximum 500 characters. Format: uri.
notify_on_submission boolean or null
Optional.
is_active boolean or null
Optional.
show_branded_footer boolean
Only paid accounts can remove branding; free accounts are forced to true.
is_encrypted boolean or null
Optional.
encryption_public_key string or null
Armoured PGP PUBLIC KEY BLOCK; required when enabling encryption unless one is already stored. Maximum 20000 characters.
encryption_key_fingerprint string 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.

type string · required
Values: short_text, long_text, email, number, phone, date, dropdown, radio, checkbox, file, heading, paragraph, divider, image, hidden, page_break, payment, submit_button.
label string or null
Required on creation except paragraph, divider, image and page_break. Maximum 255 characters.
content string or null
Paragraph HTML or image URL, according to type. Maximum 10000 characters.
description string or null
Maximum 500 characters.
placeholder string or null
Maximum 255 characters.
options string[] or null
Optional.
default_value string or null
Maximum 255 characters.
settings FieldSettings
See FieldSettings in this reference and the OpenAPI document.
logic FieldLogic
See FieldLogic in this reference and the OpenAPI document.
is_required boolean or null
Optional.
position integer 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.

type string
Values: short_text, long_text, email, number, phone, date, dropdown, radio, checkbox, file, heading, paragraph, divider, image, hidden, page_break, payment, submit_button.
label string or null
Required on creation except paragraph, divider, image and page_break. Maximum 255 characters.
content string or null
Paragraph HTML or image URL, according to type. Maximum 10000 characters.
description string or null
Maximum 500 characters.
placeholder string or null
Maximum 255 characters.
options string[] or null
Optional.
default_value string or null
Maximum 255 characters.
settings FieldSettings
See FieldSettings in this reference and the OpenAPI document.
logic FieldLogic
See FieldLogic in this reference and the OpenAPI document.
is_required boolean
Optional.

FieldSettings

min number or null
Optional.
max number or null
Optional.
step number or null
Minimum 0.
max_length integer or null
Minimum 1.
rows integer or null
Minimum 1. Maximum 50.
min_date string or null
Format: date.
max_date string or null
Format: date.
field_width string or null
Values: small, medium, large, full.
button_alignment string or null
Values: left, center, right.
accept string or null
Maximum 255 characters.
max_size integer or null
Field metadata in KB. Actual uploads have a separate server-enforced 2 MB limit. Minimum 1. Maximum 51200.
alt string or null
Maximum 255 characters.
width integer or null
Minimum 1.
image_uuid string or null
Maximum 36 characters.

FieldLogic

Send null to remove logic. Configure after creating the referenced fields.

action string
Values: show, require.
condition_type string
Values: all, any.
conditions object[]
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.

ids integer[] · required
Optional.

DuplicateForm

title string · 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.

fields object
Answers keyed by the field UUID returned by the public form. Checkbox answers are arrays of option strings.
encrypted_payload string
Armoured PGP message for encrypted forms; omit fields.
upload_uuids string[]
Optional.
client_uuid string
Format: uuid.
filled_at string
Format: date-time.
was_offline boolean
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-After when 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 fields create 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.

Ask AI about formformform

See what it does, who it’s for, and whether it fits your needs.

Opens with product context and a link to this page.

View or copy the prompt