---
title: "Public (External) Forms"
url: https://flexie.io/resources/forms/public-forms
description: "Public-facing forms anyone can fill in without logging into Flexie. Their URL, the embed snippet for your website, reCAPTCHA, analytics, file uploads, GDPR, and signed-URL private forms."
---

# Public (External) Forms

Last updated 25 May 2026

![A public Flexie form embedded on a website with a script tag, posting to Flexie on submit](https://flexie.io/image/resources/forms-public-forms.png)

A form is **External** when its Form type was set to "External" at creation. You can't change a form between External and Internal afterwards, see [Creating a form](https://flexie.io/resources/forms/creating-a-form#step-1-the-three-first-choices-you-cant-change-them-later).

## The URLs Flexie gives you

Every External form has a unique identifier and a small set of URLs built from it. You find these on the form's view page (`/forms/view/{id}`):

| URL                            | What it is                                                                                           |
| ------------------------------ | ---------------------------------------------------------------------------------------------------- |
| /form/{identifier}             | The **public form page**. Open this in any browser and you'll see the form ready to fill in.         |
| /form/push/{identifier}        | The **submit endpoint**. The form posts here when you click Submit.                                  |
| /form/upload/{identifier}      | The **file-upload endpoint**. Used internally when a submitter chooses files in a File Upload field. |
| /form/attachment/{hash}        | The **download endpoint**. Opens a previously uploaded file.                                         |
| /load-form.js?idf={identifier} | The **embed script**, see [Embedding](#embedding-the-form-on-your-website) below.                    |

You don't need to use any of these directly; they are all wired together. The two you'll share are the public form page (to send people directly to it) and the embed snippet (to put the form on your own site).

## Embedding the form on your website

The embed snippet is a single line. Open your form, copy the embed code, and paste it where you want the form to appear:

```html
<script src="https://your-flexie/load-form.js?idf=<your-form-identifier>"></script>

```

What happens when a visitor's browser hits a page with this snippet:

1. The script downloads from your Flexie instance.
2. It creates a `<div id="ff-{identifier}">` placeholder at that spot.
3. It loads the form's structure and renders the form into that placeholder.
4. When the user submits, the form posts to `/form/push/{identifier}` on your Flexie host.

You can place the snippet **anywhere** on your page: header, sidebar, inline in an article. It does not affect the rest of the page's layout beyond inserting its own container.

> If your website is on a different domain to your Flexie instance, the embed snippet's normal `<script src>` request and its form submission are _both_ permitted by browsers without extra setup, no CORS configuration is needed. CORS only matters when _your_ JavaScript posts directly to the submit URL, which the embed snippet doesn't do.

### Styling

The form ships with default styling that works on most sites. To restyle it:

* Use the **Accent colour** in form settings to match your brand.
* Add a **Custom JavaScript** block in form settings to inject scoped CSS or to hook into form events.

If you need a totally custom design, build the form on your own page and post directly to `/form/push/{identifier}` from your code, but you'll then be on your own for client-side validation.

## What the public form does when someone submits

When a visitor clicks Submit:

1. The form validates the entries in the browser (Required, Email format).
2. If a reCAPTCHA is configured, a score is fetched.
3. The form posts the JSON payload + any attachments to `/form/push/{identifier}`.
4. Flexie stores the submission, runs your workflow, and **waits up to 20 seconds** for the workflow to produce a response if one is requested. If the workflow doesn't respond, the form's default response (the Settings, Post-submit behaviour) is used.
5. The submitter sees the response: a message you wrote, a redirect to a URL, or a custom response computed by the workflow (see [Writing a custom response](https://flexie.io/resources/forms/forms-in-workflows#writing-a-custom-response-per-submission)).

The workflow listener that fires for an External form is **`virtual_workflow.form_submit`**, and the submission's field values appear in your workflow steps as `{{ __data.form_submission.<field key> }}`. See [Forms in workflows](https://flexie.io/resources/forms/forms-in-workflows#1-the-external-public-form-listener).

## Spam protection, Google reCAPTCHA v3

External forms can use **Google reCAPTCHA v3** to filter out abusive submissions. v3 is invisible: there is no checkbox or image puzzle. It scores each submission from `0.0` (almost certainly a bot) to `1.0` (almost certainly human) and Flexie rejects anything below **`0.7`**.

### To turn it on

1. **Get a reCAPTCHA v3 site key and secret key** from Google (`https://www.google.com/recaptcha/admin`). Choose **reCAPTCHA v3** (not v2), and register the domain(s) you'll embed the form on, including your Flexie host if visitors will see the form at `/form/{identifier}`.
2. **Open the form's Settings.**
3. Turn on **Add Google reCAPTCHA**.
4. Paste the **Site key** and the **Secret key**.
5. **Save and republish** the form.

From that point on, every submission is scored before the workflow runs. Submissions below the threshold are rejected with an error response and **never trigger the workflow**.

### When reCAPTCHA isn't enough

* It scores **automated** submissions; it doesn't stop a real human filling in rubbish.
* It needs JavaScript, so it doesn't help against headless submissions to `/form/push/{identifier}` direct.
* For high-risk forms (anything that auto-creates a record with permissions or triggers paid actions), pair reCAPTCHA with workflow-side checks. For example, look up whether the submitted email is on a known-bad list before creating the lead.

## File uploads, allowed types and limits

If a File Upload field is on your form, the user picks file(s) and they are sent up to Flexie when they submit.

### Allowed file types

The default allow-list is, broadly, **office documents, common images, and text**:

* **Documents**: `pdf`, `doc`, `docx`, `xls`, `xlsx`, `ppt`, `pptx`, `txt`, `csv`
* **Images**: `jpg`, `jpeg`, `png`, `gif`
* (Plus a handful more; your administrator can confirm the exact list for your account.)

Files outside the allow-list are rejected at upload time with an error to the submitter.

### Multiple files

A File Upload field can be set to **single file** or **multiple files** via the field's settings, see [Creating a form](https://flexie.io/resources/forms/creating-a-form#step-4-per-field-settings). With multiple files, the user can attach as many as the form's overall size budget allows.

### Image optimisation

If the form has **Optimise uploaded images** turned on, image attachments (`jpg` / `jpeg` / `png` / `gif`) are converted to a smaller web-friendly format on upload. This saves storage and makes downloads faster. The original file name is preserved in the submission record; only the storage format changes.

### Size limits

Per-file and per-form size limits are set by your account administrator and are ultimately bounded by your hosting configuration. If you need to accept very large files (e.g. multi-GB videos), ask your administrator to confirm what your current cap is. Most accounts use the default of "modest documents and images".

## Analytics

Add tracking IDs in the form's Settings to fire events on every submit:

* **Google Analytics tracking ID**, emits a form-submit event whenever someone successfully submits. Set **Anonymise IP** if you want GA to drop the last octet of the visitor's IP (GDPR best practice).
* **Facebook Pixel ID**, fires a `Lead` event whenever someone submits, so the conversion shows up in your Meta ads dashboard.

Both are optional and independent.

## GDPR and legal basis

External forms must declare a **legal basis** for processing the data they collect. You choose this in the form's Settings. Options include Consent, Contract, Legal obligation, Vital interests, Public task, Legitimate interests, and Pre-contractual measures. The chosen basis is recorded with every submission.

For consent-driven forms, add a **GDPR Consent** field type to the form itself (see [Creating a form](https://flexie.io/resources/forms/creating-a-form#data-fields)) and gate the **Submit** button on it being ticked.

## Private external forms (signed URLs)

If you want the form to be available _only_ via a time-limited, signed link (for example you've emailed a specific person an "update your details" form that should expire in 7 days):

1. Mark the form as **private** in its Settings.
2. Generate signed URLs using `signUrl(...)` in a Flexie Scripting expression. For example in an email template:  
```  
{% set link = signUrl("https://your-flexie/form/" ~ alias, 168) %}  
<a href="{{ link }}">Update your details</a>  
```  
(168 = 7 × 24 hours.)

A private form rejects any visit that doesn't carry a valid, in-date signature in the URL.

## What gets stored for each public submission

Every public submission writes a row that holds:

* The form it belongs to
* The submitted values (as JSON)
* Any uploaded attachments (their stored URL and original filename)
* The submitter's IP address, and an automatically-derived **country, region, city, ISP / organisation** from that IP
* The submitter's user agent (which browser they used)
* The exact submission timestamp

You view these on the form's **Submissions** page (`/forms/external/submissions/{formId}`), see [Submissions](https://flexie.io/resources/forms/submissions).

## Quick checklist before going live

1. Form is **published**.
2. **reCAPTCHA** keys are filled in (for any form accepting real public traffic).
3. The form has been **submitted at least once as a test**.
4. The workflow that fires on submit is **published** and was **tested with a real submission**.
5. The form's **default post-submit message or redirect** makes sense for the case where the workflow doesn't override it.
6. Analytics tracking IDs are in (if you want submit conversion tracking).
7. **Legal basis** is set.

## Next steps

* [Submissions](https://flexie.io/resources/forms/submissions): viewing what has come in.
* [Forms in workflows](https://flexie.io/resources/forms/forms-in-workflows): making the submission drive an automation, with a custom response.
* [Internal forms](https://flexie.io/resources/forms/internal-forms): the other flavour of form.
