---
title: "Forms API reference — formformform"
description: "Create and edit forms with the formformform REST API. API keys, working examples, field schemas, conditional logic, submissions and OpenAPI for AI agents."
url: "https://www.formformform.com/api"
type: static
generatedAt: "2026-10-04T22:33:41.644Z"
---
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 an API key ↗](https://app.formformform.com/settings/api-keys)[OpenAPI 3.1 JSON](https://www.formformform.com/api/openapi.json)[Read as Markdown](https://www.formformform.com/api.md)

## Create and edit a form

Open [Settings → API Keys](https://app.formformform.com/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

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

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

```bash
# 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](https://app.formformform.com/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](https://www.formformform.com/api/openapi.json) describes the returned properties.

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

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

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

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

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

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

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

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

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

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

`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”:

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

```json
{
  "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](https://www.formformform.com/api.md) or [OpenAPI schema](https://www.formformform.com/api/openapi.json) 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](https://app.formformform.com/api/mcp/contract) describes its capabilities. Its [GPT Actions schema](https://app.formformform.com/api/mcp/openapi.json) deliberately omits editing operations. Use [this reference’s OpenAPI schema](https://www.formformform.com/api/openapi.json) 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](https://www.formformform.com/help/rest-api). For submission delivery, see [webhooks](https://www.formformform.com/help/webhooks).
