# HTML form

Use a plain HTML form on your own site to send answers to an InvoyForm form: the endpoint, the field names, what comes back, and the limits.

Build the form with InvoyForm, then post to it from a form you wrote yourself. You control the markup and the CSS. InvoyForm checks the answers and stores them.

## The form

Publish the form, open **Share**, and copy the **HTML form**. It looks like this:

```html
<form action="https://form.invoy.in/api/public/forms/YOUR_FORM_ID" method="POST">
  <label>
    Full name
    <input type="text" name="full_name" required>
  </label>
  <label>
    Email
    <input type="email" name="email" required>
  </label>
  <button type="submit">Submit</button>
</form>
```

It has your questions' help text, their starting values and the button text you chose in Settings. It is a starting point. Change the markup and style it as you like, but keep each field's `name`.

## Allowed websites

Your form's id is in the HTML code you put on your page, so anyone who sees it could use your form from their own site. To stop that, a form works only on websites you allow. Your form's own link always works.

Open **Settings** in the builder, add the websites where you use the form, and publish. Only those websites can show the form in a frame (an embed) or send answers from their own HTML form or script. Posts from anywhere else, including a script with no website behind it, are refused with a `403`.

- `my-site.com` allows that site only. `www.my-site.com` is another site: add it too, or add `*.my-site.com` for all its subdomains.
- The site must use `https`. `localhost` (to test on your machine) always works.
- The list takes effect when you publish.

## The script

The code from Share also has a `<script>` line and `data-ivf` on the form:

```html
<form action="https://form.invoy.in/api/public/forms/YOUR_FORM_ID" method="POST" data-ivf>
  ...
</form>
<script src="https://form.invoy.in/v1/embed.js" defer></script>
```

With it the page does not reload. The script sends the form, turns the button off while it works, shows each answer that is not accepted under its field, and shows a thank-you when it is done. Without it the form still works as a normal post.

You can shape it with attributes. None are needed.

| Attribute | What it does |
| --- | --- |
| `data-ivf-error="email"` on any element in the form | Shows the message for the field `email` there, instead of adding one under the field |
| `data-ivf-form-error` on an element | Shows messages that are about the whole form (too many attempts, no connection) |
| `data-ivf-success` on a hidden element | Shown instead of the default thank-you |
| `data-ivf-redirect="https://..."` on the form | Goes to that address after a successful send. Without it, the form's own redirect address (if you set one for the form) is used |
| `data-ivf-state` on the form (set by the script) | `idle`, `submitting`, `error` or `success`, for your CSS |

Added messages have the class `ivf-error` (the thank-you has `ivf-success`) and a plain default look. Style them with your own CSS. A field with a message has `aria-invalid="true"`.

The script uses the browser's own checks (`required`, `type`, `min`, `max`...) and shows them in the same place. Every other rule is checked by InvoyForm after the send.

### Events

The form tells your page what happened, so your code can close a dialog, track a conversion or show its own message. Both events bubble.

| Event | When | `event.detail` |
| --- | --- | --- |
| `ivf:success` | The answers were accepted | `{ redirectUrl }`, where the person is sent next, if anywhere |
| `ivf:error` | The answers were not accepted | `{ errors: [{ field, message }] }`, or `{ message }` for the whole form |

```js
form.addEventListener('ivf:success', () => closeDialog());
```

### React, Vue and other frameworks

The script also finds forms that appear after the page has loaded, like a form in a dialog or on a page your router shows later. Load the script once, render the form with `data-ivf`, and let it submit natively: no `onSubmit` handler and no state for the values. To have your agent do it for you, see [Build it with your AI agent](https://form.invoy.in/docs/developer/ai-agents/).

If your site sends a Content Security Policy, allow `https://form.invoy.in` in `script-src` (the script) and `connect-src` (where it sends the answers), and in `frame-src` if you use the [embed](https://form.invoy.in/docs/developer/embed/).

## Field names

Every field that takes an answer has a `name`, made from its label as you build ("Full name" becomes `full_name`). A repeated name gets `_2`, `_3`. A field with no label uses its placeholder, then its type. You can change a name in the field's **Advanced** tab.

- Post each answer under its field's `name`.
- A group of checkboxes shares the field's name with `[]`: `name="interests[]"`.
- A group of radio buttons shares the field's name.
- Once the form is published a name stays as it is, even if you edit the label. Change it yourself in the Advanced tab if you must, and update the HTML forms that use it.

Names that start with `_ivf_` are InvoyForm's own, never a field's. They are not kept with the answer. Any other name that starts with `_` is refused.

| Name | What it is |
| --- | --- |
| `_ivf_password` | The form's password, when it has one. Let the respondent type it: `<input type="password" name="_ivf_password">`. Never put it in the page's HTML, where anyone can read it. A script that sends the answers itself can send it this way too. |

## After you submit

- A browser that posts the form lands on the form's thank-you page, in the form's theme, or on the form's redirect address if you set one.
- If an answer is not accepted, the browser is sent back to the form page with the questions that need another look marked. The form is empty again (what was typed is never kept in the address), so the respondent fills it in again.
- A script (`fetch`, `curl`) gets JSON back instead.

For a script, an answer that is not accepted is a `400`, and `details` says which field:

```json
{
  "success": false,
  "message": "Some of the answers need another look.",
  "details": [
    { "field": "email", "message": "Please enter a valid email address." }
  ]
}
```

`field` is the field's `name`.

## Files

A form with a File Upload question gives you an `<input type="file">`. Only the Invoy script can send it (the HTML from the Share page includes it): it sends each chosen file first, then posts the form with the files' ids in their place. A plain form without the script cannot send files.

If a file is not accepted (too big, a kind the question does not take), the message shows next to its field.

## Limits

- The generated HTML is a copy. If you add a required field to the form later, copy the HTML again, or posts without that field are rejected.
- Logic (showing a field only when another has a value) and widgets that need JavaScript are not part of the plain HTML.
- Your plan sets how many HTML posts a month a workspace can receive.
