Skip to content

Forms as code

Keep forms and their destinations in a quickforms.json file and deploy them with the CLI, a GitHub Action or a Netlify build plugin.

Keep your forms in a quickforms.json file next to your site and deploy them from the command line or CI. Each form has a key, such as contact. Deploying creates the form with that key the first time and updates the same form after that, so the same file always gives the same form and it is safe to deploy on every build.

Everything goes through one endpoint, PUT /api/v1/forms/by-key/{key}, with a team API key. The CLI, the GitHub Action and the Netlify plugin all call it.

The file

quickforms.json has a forms object keyed by form key. Each value is a form definition.

quickforms.jsonjson
{
  "forms": {
    "contact": {
      "name": "Contact",
      "description": "General enquiries from our website.",
      "schema": {
        "fields": [
          { "name": "name", "type": "text", "label": "Name", "required": true },
          { "name": "email", "type": "email", "label": "Email", "required": true },
          { "name": "message", "type": "textarea", "label": "Message", "required": true, "min": 10 }
        ]
      },
      "settings": { "notify": { "emails": ["you@example.com"] }, "allowed_origins": ["https://example.com"] },
      "destinations": [
        { "name": "Team Slack", "type": "slack", "config": { "webhook_url": "${SLACK_WEBHOOK_URL}" } }
      ]
    }
  }
}

This is the starter file quickforms init writes.

Keys

A key is 1 to 64 lowercase letters, numbers, dashes and underscores, starting with a letter or number, like contact or beta-waitlist. Keys are unique within a team, and a deleted form’s key can be used again. Anything else is a 422 on key.

Form definitions

A definition takes the same attributes as POST /api/v1/forms, and is validated the same way:

Key Description
name Required when the form is first created.
description What the form is for, used by the spam filter.
mode live or sandbox.
schema The fields. See schema. Required when the form is first created.
is_active true or false.
webhook_url Where the webhook goes.
mail_provider_id One of the team’s mail providers.
settings See form settings. Settings are merged into the form’s current settings, and lists are replaced.
turnstile_secret Write-only Turnstile secret key.
stripe_secret Write-only Stripe secret key for payments.
destinations A list of destinations.

Two more keys control the deploy. The CLI sets them for you from its flags:

Key Description
draft true keeps the changes as a draft instead of making them live. A form created with draft stays unpublished until you publish it.
prune_destinations true deletes the form’s destinations that are not in destinations. Off by default, so destinations added in the dashboard are kept.

Destinations

Each item in destinations has a name (required, up to 80 characters, unique in the list) and a type (required), plus enabled, config, mapping and conditions, the same as POST /api/v1/forms/{form}/destinations. A form can have up to 20 destinations. See destinations for each type’s config.

Destinations are matched to the form’s existing ones by name. A name that exists is updated, a new name is created. The type of an existing destination doesn’t change. Leave destinations out to leave the form’s destinations alone.

Errors in a destination come back with its position, for example destinations.0.config.access_token. When any part of a definition is invalid nothing is saved.

Environment variables

The CLI replaces ${NAME} in any string with the environment variable NAME (uppercase letters, numbers and underscores) before sending the definition. Use it for secrets such as webhook URLs, API keys, turnstile_secret and stripe_secret, so they stay out of the file. A variable that is not set stops the deploy with an error.

The CLI

The CLI is a Node.js script with no dependencies. It needs Node 18 or later. It lives in the quickforms repository at packages/cli (package name @quickforms/cli, command quickforms). Run it from a checkout of the repository:

Run from the repositorybash
node packages/cli/bin/quickforms.mjs deploy --file path/to/quickforms.json

Or install the folder globally to get the quickforms command:

Install from the repositorybash
npm install -g ./packages/cli
quickforms deploy

Commands

Command What it does
quickforms init Write a starter quickforms.json. Fails if the file already exists.
quickforms validate Check every form without saving anything (a dry run).
quickforms deploy Create or update every form in the file, and its destinations.
quickforms pull Write the team’s keyed forms to quickforms.json.

Options

Option Default Description
--file quickforms.json The config file.
--host $QUICKFORMS_URL Your quickforms URL.
--token $QUICKFORMS_TOKEN A team API key.
--only All forms Comma-separated form keys to deploy or validate, for example --only contact,newsletter.
--draft Off validate and deploy: save changes as drafts instead of publishing.
--prune Off validate and deploy: remove destinations that are not in the file.
--json Off validate and deploy: print the results as JSON instead of one line per form.

validate, deploy and pull need a host and API key, from the options or the QUICKFORMS_URL and QUICKFORMS_TOKEN environment variables.

export QUICKFORMS_URL=https://YOUR-QUICKFORMS-HOST
export QUICKFORMS_TOKEN=YOUR_API_KEY
export SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...

quickforms validate
quickforms deploy

deploy prints one line per form: created, updated or (for validate) valid, then the key, status and endpoint URL, plus how many destinations were added, updated or removed. With --json it prints a list with each form’s key, id, status, endpoint_url, hosted_url, sandbox_key and created.

If a request fails, the CLI prints the status code, the message and each validation error, and exits with code 1.

Pull

quickforms pull writes every form that has a key to the file, with its name, description, mode, schema, settings, webhook_url and destinations. Forms without a key are skipped. Masked secrets in destination config are left out, so add them back as ${ENV_VARS} before you deploy. It overwrites the file.

The endpoints

Both need a team API key, like the rest of the REST API.

PUT /api/v1/forms/by-key/{key}

Create or update the form with this key from a definition. Add ?dry_run=1 to validate everything without saving: the form and destinations are checked exactly as a real deploy would, then thrown away.

curl -X PUT https://YOUR-QUICKFORMS-HOST/api/v1/forms/by-key/contact \
  -H "Authorization: Bearer $QUICKFORMS_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"name": "Contact", "schema": {"fields": [{"name": "email", "type": "email", "required": true}]}, "destinations": [{"name": "Team Slack", "type": "slack", "config": {"webhook_url": "https://hooks.slack.com/services/..."}}]}'
{
  "data": {
    "id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
    "key": "contact",
    "name": "Contact",
    "status": "published",
    "endpoint_url": "https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p",
    "hosted_url": "https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p",
    "...": "...",
    "destinations": [
      { "id": "01jcd0...", "form_id": "01jc8xk2m5t9qw3e7r4y6u8v0p", "type": "slack", "service": "Slack", "name": "Team Slack", "enabled": true, "config": { "webhook_url": "••••••••..." }, "mapping": { "...": "..." }, "conditions": [], "...": "..." }
    ]
  },
  "meta": {
    "dry_run": false,
    "created": true,
    "status": "published",
    "destinations": { "created": ["Team Slack"], "updated": [], "deleted": [] }
  }
}

data is the form, the same as GET /api/v1/forms/{form}, plus its destinations. meta.created says whether the form was new, and meta.destinations lists the destination names created, updated and deleted. It responds 201 when it created the form and 200 when it updated one or on a dry run.

GET /api/v1/forms/by-key/{key}

The form with this key, with its destinations. 404 if the team has no form with that key.

curl https://YOUR-QUICKFORMS-HOST/api/v1/forms/by-key/contact \
  -H "Authorization: Bearer $QUICKFORMS_TOKEN" \
  -H "Accept: application/json"

Dry runs

quickforms validate and ?dry_run=1 run the whole deploy, including schema and destination validation, without saving. Errors come back as a 422, the same as a real deploy. meta.dry_run is true.

Drafts

With draft (--draft in the CLI), changes to a live form are kept as a draft and status becomes changes_pending. The live version keeps taking submissions until you publish. Destinations are saved straight away, they are not part of a draft.

GitHub Action

The CLI folder is also a GitHub Action (packages/cli/action.yml). It sets up Node 20 and runs the CLI.

Input Required Default Description
host Yes Your quickforms URL.
token Yes A team API key. Store it as a secret.
file No quickforms.json The config file.
command No deploy deploy or validate.
draft No false Save changes as drafts instead of publishing.
prune No false Remove destinations that are not in the file.

It has one output, forms: the JSON list of forms the CLI’s --json option prints, with their endpoint URLs.

Point uses at the packages/cli folder of the quickforms repository. This workflow validates on pull requests and deploys on pushes to main:

.github/workflows/quickforms.ymlyaml
name: quickforms

on:
  pull_request:
  push:
    branches: [main]

jobs:
  forms:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - id: quickforms
        uses: OWNER/quickforms/packages/cli@REF
        with:
          host: https://YOUR-QUICKFORMS-HOST
          token: ${{ secrets.QUICKFORMS_TOKEN }}
          command: ${{ github.event_name == 'pull_request' && 'validate' || 'deploy' }}
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      - run: echo '${{ steps.quickforms.outputs.forms }}'

Replace OWNER/quickforms and REF with the repository and the branch, tag or commit you use. Environment variables used as ${NAME} in quickforms.json need to be set on the step.

Netlify build plugin

netlify-plugin-quickforms (in packages/netlify-plugin-quickforms) deploys quickforms.json before your site builds. It uses the CLI package, @quickforms/cli.

netlify.tomltoml
[[plugins]]
package = "netlify-plugin-quickforms"

  [plugins.inputs]
  file = "quickforms.json"
  draftOnPreview = true
Input Default Description
file quickforms.json The config file.
draftOnPreview true Builds outside the production context (such as deploy previews) save changes as drafts instead of publishing.

Set QUICKFORMS_URL and QUICKFORMS_TOKEN, and any ${NAME} variables the file uses, in your Netlify environment variables.

Before the build it:

  1. Deploys every form in the file. In a context other than production, with draftOnPreview on, it deploys them as drafts.
  2. Sets a QUICKFORMS_<KEY>_URL environment variable for each form with its endpoint URL, so your build can point forms at the right address. The key is uppercased and anything other than letters and numbers becomes _: beta-waitlist becomes QUICKFORMS_BETA_WAITLIST_URL.
  3. Writes quickforms.forms.json to the publish directory, a list of each form’s key, endpoint_url and hosted_url.
  4. Shows how many forms it deployed in the deploy summary.

If the deploy fails, the build fails with the error.

Type to search…

↑↓ navigate↵ selectEsc close