Using the REST API

A webhook pushes a submission to you the second it lands. The REST API works the other way round: your code asks when it suits — a nightly job pulling yesterday's callout reports into a rota system, a script that spins up a fresh intake form per client.

Everything sits under https://app.formformform.com. Authentication is a bearer token you fetch by signing in over the API; there is no API-key screen in Settings, and the token reaches as far as your account does. Resources come back wrapped in a data key.

Sign in for a token

You exchange your account password for a token once, then send that token on every later call. Nothing expires on a timer, so a token left in a cron job keeps working until POST /api/v1/auth/logout revokes it — and that call revokes only the token that made it.

  1. 1

    POST to https://app.formformform.com/api/v1/auth/login with a JSON body holding email and password.

  2. 2

    Add device_name to label the token — "rota-sync", say. Left out, it is labelled "mobile".

  3. 3

    With two-factor authentication on, the reply holds two_factor: true and a challenge_token instead. POST that token and your six-digit code to /api/v1/auth/two-factor-challenge; a recovery_code works in place of code, and is spent once used.

  4. 4

    Store token where your code can read it and others cannot.

  5. 5

    Send it as Authorization: Bearer <token>, alongside Accept: application/json.

  6. 6

    Confirm it works with GET /api/v1/auth/me.

Five wrong passwords for the same email and IP inside a minute returns 429. The account's email must also be verified: an unverified account gets 403 on forms and submissions even though login itself succeeds.

What the API covers

Under /api/v1, with your token: forms (list, create, read, update, delete, plus /duplicate and /toggle-active), a form's fields (create, update, delete, reorder), submissions (list, read, delete, export), and /api/v1/templates. Under /api/v1/public, with no token: read an active form and post an answer to it.

Which identifier goes where is what trips people up. A form is addressed by its uuid, already in the editor's address bar at /form-builder/<uuid>/edit, and a submission by its own uuid. Fields are the exception, taking the integer id the API hands back. Public routes use a third value, the short public_id from the share link.

Creating a form needs only title; description, thank_you_message, redirect_url, notify_on_submission, is_active and template are optional, template taking a slug (default blank) to seed the fields. slug is derived from the title, public_id never changes, and both are ignored if sent.

Read responses

GET /api/v1/forms/<uuid>/submissions returns newest first, 25 to a page, per_page adjustable up to 100, with today_count alongside. Each row holds uuid, created_at, ip_address, user_agent and a values array — one entry per answered field, giving field_uuid, field_type, field_label and value. Fields left blank or hidden by conditional logic are absent rather than null, so read by field_uuid, never by position.

Fetching one submission adds prev and next uuids for walking the list, plus the armoured encrypted_payload on an encrypted form. /submissions/export streams the same CSV as the Submissions tab: a column per input field, then Submitted At, IP Address, Filled At and Was Offline.

A File Upload answer's value is the upload's uuid, not a link. The bytes come from the owner-only download route behind the Submissions tab, which reads a browser session rather than a bearer token.

Post a submission, uploads included

GET /api/f/<public_id> returns the live form: title, thank-you message, and every field with its integer id, type, options and is_required flag. POST back to the same address with a fields object keyed by those ids. The reply carries submission_uuid plus the thank-you message and any redirect. Throttled at 30 posts a minute per IP.

A file goes up on its own, before the submission exists. POST it as multipart to /f/<public_id>/uploads with form_field_id set to the File Upload field, read the uuid from the reply, then send that uuid twice: as the field's value inside fields, and in the upload_uuids array. Up to 50 files per submission. An encrypted form takes an armoured encrypted_payload in place of fields.

2 MB per file, enforced on the server: a field's settings.max_size accepts a larger number through the API, but anything above 2 MB is still refused. Prefer /api/f/<public_id> over /api/v1/public/forms/<public_id>/submissions when the form has a File Upload field — the v1 route stores the answers but does not attach the uploads.

Frequently asked questions

Is there an API key I can generate in Settings?

No. Settings holds Profile, Password, Two-Factor Auth, Appearance, Billing and Zapier, and none of them mints a key. The token comes from POST /api/v1/auth/login, with device_name as its only label. Call /api/v1/auth/logout with a token to revoke it.

Can I download the files people uploaded through the API?

No. The submission gives you each upload's uuid, but the download and preview routes are owner-only and read a browser session, not a bearer token. Open the submission in the Submissions tab instead. On an encrypted form the stored bytes are ciphertext anyway.

Can I set up a webhook through the API?

No. A webhook URL and its signing secret belong to one form and are set in the editor, under the Settings tab then the Integrations sub-tab. No endpoint reads or writes them, so a script that creates forms still leaves someone a trip to the editor.

Related articles

Nothing here answering your question? The editor is quick enough that trying it is often faster than reading about it.

Open the editor