---
title: "Embed"
description: "Render a form from its schema with one script tag, or drop in an iframe."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.getquickforms.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Embed

Render a form from its schema with one script tag, or drop in an iframe.

## Script

```html title="anywhere in your page"
<div data-quickforms="FORM_ID"></div>
<script src="https://YOUR-QUICKFORMS-HOST/embed.js" defer></script>
```

The script finds every `[data-quickforms]` element, loads the form's public schema and renders every field type with labels, help text, required markers and every [honeypot](https://docs.getquickforms.com/spam-protection/#honeypots), hidden. It posts with `fetch`, shows validation errors next to each field and the success message when it worked. Because it posts from your page, your site's origin must be allowed by the form's [CORS settings](https://docs.getquickforms.com/submit/#cors).

If the form has [Cloudflare Turnstile](https://docs.getquickforms.com/spam-protection/#cloudflare-turnstile) switched on, the script also loads `https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit`, renders the Turnstile widget with the form's site key and sends its token as `cf-turnstile-response`. After a failed submission it resets the widget so the visitor can try again. Your site's hostname must be on your Turnstile widget in Cloudflare. With `data-sandbox`, the widget is not rendered, because sandbox submissions skip Turnstile.

For containers added after page load, call `window.quickforms.mount(element)` or `window.quickforms.boot()`.

When the uploads disk can sign uploads, the widget uploads each chosen file straight to storage first and sends the form as JSON with the upload IDs. Otherwise it sends file fields as multipart. For your own forms, `window.quickforms.upload(file, options)` uploads a file straight to storage and resolves with an `upl_...` ID to submit as the field's value. See [direct uploads](#direct-uploads).

## Options

| Attribute | Description |
| --- | --- |
| `data-quickforms` | The form ID. Required. |
| `data-sandbox` | A sandbox key (`qf_sandbox_...`). The widget renders the form's [draft](https://docs.getquickforms.com/drafts-and-publishing/), so you can preview unpublished changes, submissions are sent in sandbox mode and a note says nothing will be delivered. |
| `data-submit-label` | Button text. Defaults to "Submit". |
| `data-redirect` | Go to this URL after a successful submission. Must be `http` or `https`; anything else is ignored. |
| `data-unstyled` | Skip the default styles and style the `qf-*` classes yourself. |

## Pin the script with SRI

`/embed.js` is the moving version: it is cached for 5 minutes and always has the latest fixes. To pin a release, use the versioned copy under `/embed/v1/` with Subresource Integrity, so the browser refuses the file if it ever changes. Copies are immutable and cached for a year. You get new versions by updating the snippet.

The dashboard's **Embed script with SRI** panel and the API's `embed.script_sri` give you the exact tag:

```html title="anywhere in your page"
<div data-quickforms="FORM_ID"></div>
<script src="https://YOUR-QUICKFORMS-HOST/embed/v1/embed.HASH.js" integrity="sha384-..." crossorigin="anonymous" defer></script>
```

When the script is loaded from somewhere other than your quickforms host, such as a CDN, add `data-host="https://YOUR-QUICKFORMS-HOST"` to the tag so it knows where forms live. Snippets include this when `QUICKFORMS_CDN_URL` is set.

## Direct uploads

`window.quickforms.upload(file, { form, field, sandbox, host })` asks for a pre-signed upload URL, `PUT`s the file to it and resolves with the upload ID. Use it in a custom form that submits JSON.

```js
const id = await window.quickforms.upload(input.files[0], { form: "FORM_ID", field: "cv" });
// Then submit JSON with { "cv": id, ... } to https://YOUR-QUICKFORMS-HOST/f/FORM_ID
```

| Option | Description |
| --- | --- |
| `form` | The form ID. Required. |
| `field` | The file field's name. Required. |
| `sandbox` | A sandbox key, sent as `X-Quickforms-Sandbox`. Needed for unpublished forms. |
| `host` | The quickforms host. Defaults to the host `embed.js` was loaded from. |

It rejects with the JSON error body if the upload URL can't be issued, and with `{ message: "The upload failed. Please try again." }` if the `PUT` fails. See [file uploads](https://docs.getquickforms.com/file-uploads/#the-quickformsupload-helper) for the full flow, limits and bucket CORS.

## Theming

Set CSS variables on the container. These are the defaults:

```css title="styles.css"
[data-quickforms] {
  --qf-accent: #0b0b0f;   /* button and focus ring */
  --qf-ink: #0b0b0f;      /* text */
  --qf-muted: #6b6b76;    /* help text */
  --qf-line: #dcdce0;     /* input borders */
  --qf-bg: #fff;          /* input background, button text */
  --qf-radius: 10px;
  --qf-danger: #d93636;   /* errors and required marker */
}
```

With `data-unstyled`, target the classes directly: `.qf` (the form), `.qf-field`, `.qf-label`, `.qf-input`, `.qf-help`, `.qf-error`, `.qf-invalid`, `.qf-button`, `.qf-status`, `.qf-status-ok`, `.qf-status-err` and `.qf-hp` (the honeypot wrapper, keep it hidden) and `.qf-turnstile` (the Turnstile widget).

## Events

The container dispatches `quickforms:success` and `quickforms:error`. `event.detail` is the JSON response body.

```js
const el = document.querySelector('[data-quickforms]');

el.addEventListener('quickforms:success', (event) => {
  console.log('Submitted', event.detail.id, event.detail.sandbox);
});

el.addEventListener('quickforms:error', (event) => {
  console.log(event.detail.message, event.detail.errors);
});
```

## Iframe

Every form also has a hosted embed page at `/f/{form}/embed` that any site may frame. Add `?sandbox=qf_sandbox_...` to preview the draft and send sandbox submissions from it.

The page uses the same script, so it renders the honeypots and the Turnstile widget too. The widget runs on the quickforms page rather than on your site, so if you use Turnstile, add your quickforms app's hostname to your Turnstile widget in Cloudflare.

```html
<iframe src="https://YOUR-QUICKFORMS-HOST/f/FORM_ID/embed" width="100%" height="600" style="border:0" title="Contact"></iframe>
```

## Public schema JSON

`GET /f/{form}/schema.json` returns what the widget renders from, cached for 60 seconds. Custom error messages are left out. Use it to build your own renderer.

| Key | Description |
| --- | --- |
| `honeypot` | The main honeypot name. |
| `honeypots` | Every honeypot name: the main one first, then any extra ones. Render them all, hidden. |
| `turnstile` | `{"site_key": "..."}` when the form has Turnstile switched on, otherwise `null`. Always `null` for sandbox previews. |
| `success_message` | The form's success message, with any `{{field_name}}` placeholders unfilled. The submit response has the filled-in version. |

It returns the published version. Add `?sandbox=qf_sandbox_...` to get the [draft](https://docs.getquickforms.com/drafts-and-publishing/#test-a-draft) instead, sent with `Cache-Control: no-store`. A form that has never been published returns a `404` with "This form has not been published yet." unless the sandbox key is given.

```json title="GET https://YOUR-QUICKFORMS-HOST/f/FORM_ID/schema.json"
{
  "id": "01jc8xk2m5t9qw3e7r4y6u8v0p",
  "name": "Contact",
  "action": "https://YOUR-QUICKFORMS-HOST/f/01jc8xk2m5t9qw3e7r4y6u8v0p",
  "active": true,
  "honeypot": "_gotcha",
  "honeypots": ["_gotcha", "website", "fax"],
  "turnstile": { "site_key": "YOUR_SITE_KEY" },
  "success_message": "Thanks! Your submission has been received.",
  "fields": [
    { "name": "email", "type": "email", "label": "Email", "required": true },
    { "name": "message", "type": "textarea", "label": "Message", "required": false, "max": 2000 }
  ]
}
```

## Filling in answers from the page address

Add `data-prefill="url"` to the embed's container and any query parameter on the host page that matches a field name fills in that question. It's off by default, because a parameter on your own site could share a name with a field.

Source: https://docs.getquickforms.com/embed/index.mdx
