---
title: "How forms work"
description: "Sign-up forms put people into your audiences. Build one here, share it as a link, embed it on any site, or post to it from your own code."
url: "https://openemail.uk/docs/api/forms/overview"
area: "API"
category: "Mailbox"
---

# How forms work

Sign-up forms put people into your audiences. Build one here, share it as a link, embed it on any site, or post to it from your own code.

## A draft and a live copy

A form keeps two copies of what visitors see. `document` is the draft you edit, and `publishedDocument` is what the hosted page, the embed and the subscribe endpoint use. Saving changes only the draft, and `POST /forms/{id}/publish` copies it live. `hasUnpublishedChanges` tells you the two differ.

- `draft`: never published. Nobody can see it or sign up through it.
- `live`: published and taking sign-ups.
- `paused`: published but closed. The page shows the closed message from its copy and sign-ups are refused.

`settings` are different: where sign-ups go, double opt-in, the sender, the thank-you behaviour and who is told about each sign-up. They apply as soon as they are saved, published or not.

## Fields

A document is a list of `fields`, the `copy` around them and a `style`. Each input field has a `key`, which is the name its answer is posted under: a lower-case letter followed by up to 39 lower-case letters, digits or underscores, unique in the form, and never starting with `oe_`. Every form has exactly one `email` field, keyed `email` and required.

- Inputs: `email`, `text`, `textarea`, `number`, `phone`, `url` and `date`.
- Choices: `select`, `radio` and `checkboxes`, each with `options`.
- `checkbox` for a yes or no, and `consent` for a box that has to be ticked when it is required.
- `audiences` lets the person choose lists: each option `value` is an audience id in this workspace.
- `hidden` carries a value the visitor never sees: the one your page posts, or else its `defaultValue`, such as a campaign name.
- `heading`, `paragraph` and `divider` only lay the form out and post nothing.

Set `mapsTo` to `firstName`, `lastName` or `name` on a text field, and the answer becomes the name of the contact the sign-up creates. A contact that already exists keeps its name. Every answer is kept on the submission, with the label it had, so old submissions still read right after the form changes.

## Putting a form on a page

Publish first. Then use whichever of the three suits the page. All of them reach the same form and count the same sign-ups. Views are counted only on the hosted page and the embed, so sign-ups through your own HTML or code raise the conversion rate.

- The hosted page at `url`, a page of its own you can link to from anywhere.
- The embed script, which puts the form on your page in a frame that sizes itself.
- Your own HTML or code, posting the answers to `subscribeUrl`.

**Embed**

```
<script src="https://openemail.uk/embed/form.js" data-openemail-form="frm_3b9d2e7a1c4f80d56e2a9b14" async></script>
```

**HTML**

```
<form action="https://api.openemail.uk/subscribe/frm_3b9d2e7a1c4f80d56e2a9b14" method="post">
  <input type="email" name="email" required>
  <div style="position:absolute;left:-9999px" aria-hidden="true">
    <input type="text" name="oe_website" tabindex="-1" autocomplete="off">
  </div>
  <button type="submit">Subscribe</button>
</form>
```

A plain HTML form is redirected to the thank-you page, or to `settings.redirectUrl`. Code that posts JSON gets a JSON answer instead, described on the subscribe page.

## Double opt-in

With `settings.doubleOptIn` on, a sign-up is stored as `pending` and the person is emailed a link from `settings.senderAddress`, an address of this workspace. They join the audiences when they open it. The link works for seven days. A person who unsubscribed from an audience earlier is only subscribed again this way, never by a single opt-in form. Signing up again before confirming updates the pending sign-up rather than adding another.

To protect the people you email, one address gets at most one confirmation per form every ten minutes and five a day across the workspace. You can approve a pending sign-up yourself, or send it a fresh link.

## Who can see what

- Reading needs `forms:read` and changing needs `forms:write`. Approving a sign-up also needs `contacts:write`, because it adds a contact.
- Anything that makes a form send mail also needs `emails:send`: turning on double opt-in, setting the sender or the confirmation email, publishing or resuming a double opt-in form, and resending a confirmation.
- An API key and the owner see every form in the workspace. An app a member connected sees only the forms that member made, and only the audiences that member made plus the built-in ones.
- Creating, updating, publishing, resuming or duplicating a form whose sender or notify addresses lie outside what a limited key or app may reach answers 422 `capability_unsupported`.
- A key or app limited to some addresses may only set a sender and notify addresses it holds.
- Deleting a form asks an OAuth app for a verification code, as other destructive changes do. An API key never needs one.

`form.submitted` and `form.confirmed` webhooks tell your systems about every sign-up. A webhook limited to some addresses never receives them, because sign-ups belong to the whole workspace.

## Bots and limits

- A field named `oe_website` is a trap for bots: keep it empty and off screen, as the HTML above does. A sign-up that fills it gets a normal answer and is dropped.
- The hosted page and the embed also check a signed start time, and a form sent back faster than a person could fill it in is dropped the same way.
- One network can post 40 sign-ups in ten minutes, across all your forms and whatever the outcome. After that JSON callers get 429 `form_rate_limited`, and a plain HTML form goes to the hosted page with `?outcome=limited`.
- A workspace holds 100 forms by default.

## From code, the terminal and agents

Everything here is also in the SDK as `openemail.forms` and in the CLI as `openemail forms`, and the MCP server has form tools, so an agent can build, publish and watch a form. Over MCP the client writes the design itself and passes it as `document`.

Posting to `subscribeUrl` from your own code needs no credential. Send the answers as JSON, add the page the form was on as `oe_source`, leave out `oe_started`, and send `oe_website` empty or not at all. Every sign-up from one network shares the limit of 40 every ten minutes, so a server relaying sign-ups for many people reaches it quickly: add people you already know with the audience import instead.
