Skip to the documentation
API

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 on a field and its answer is saved on the contact the sign-up creates: firstName, lastName, name, company or jobTitle on a text field, phone on a phone field, website on a URL field and birthday on a date field. A contact that already exists keeps its name and whatever its card already holds, and a phone number or website it already lists is not added twice. Every answer is kept on the submission, with the label it had, so old submissions still read right after the form changes.

Set saveAs on a field to save its answer to a custom field of the contact, such as plan, which a broadcast reads as {{attributes.plan}} and a segment can match. Each answer replaces the field of that name, an empty answer leaves it alone, and a choice of several options is saved as their labels joined by commas. It works on any field that takes an answer except the email and audiences, one field per custom field, and not together with mapsTo.

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-signup-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 earlier is only subscribed again this way, never by a single opt-in form: a form without double opt-in sends them the same confirmation email instead, when it has a sender address. 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.

A new form has double opt-in on, sending from the first address you may send as. Turn it off in the form settings, or send settings.doubleOptIn: false, for a form that adds people at once. Forms made before this keep their setting.

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.
  • A form can list the sites that may use it, in its Settings or as settings.allowedSites: a domain (example.com), every subdomain of one (*.example.com), or either with a path (example.com/pricing, example.com/blog/*). The embed and the iframe then show only on those sites, and a post from any other site answers 403 form_site_not_allowed. A browser names its site by itself, a server names the page in oe_source, and the form's own page always works. This keeps other websites out. It is not a secret, since anything that is not a browser can send any address.

From code, the terminal and agents

Everything here is also in the SDK as nextmail.forms and in the CLI as nextmail 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.