Collecting signups
Waitlists and newsletters from any website: a link, one line, your own HTML or code.
A signup form lets people join a waitlist or a newsletter from any website, without you building a backend. You make the form in the dashboard (Marketing → Signup forms), choose the list people join, and put it on your site in one of four ways. Everyone who joins lands on that list, and any automation that starts "when someone joins the list" sends your welcome email.
This guide is for the developer putting the form on a site. The dashboard's Share tab shows every snippet below with your form's real id already in it.
The form id
Every form has a public id like frm_8kq2xv9btn4lw0. It is not a secret: knowing it lets you sign up,
which is the point. It cannot read anything, and it only ever adds to the lists the form names.
Nothing below needs an API key. A form only takes signups while it is Live.
1. A link
https://mailyte.com/f/<form id> is a page we host with your logo, words and colour. Put it in a
post, a bio, an email or a QR code. It is not indexed by search engines.
On your own address
On Growth plans and up, the page can live on your own subdomain, like join.acme.com. On the form's
Share tab, under Your own address, enter the subdomain; it must sit on a domain you have verified
on the Domains page. Then add one DNS record:
| Type | Name | Value |
|---|---|---|
| CNAME | join |
forms.mailyte.com |
On Cloudflare, set it to DNS only (grey cloud). We look for it every ten minutes, then set up the secure certificate. The Share tab shows each step, and the page goes live on its own.
2. One line on your page
<script async src="https://mailyte.com/embed/v1.js"
data-mailyte-form="frm_8kq2xv9btn4lw0"></script>The form appears where the tag is and sizes itself. To put it somewhere else, add
<div data-mailyte-form-mount="frm_8kq2xv9btn4lw0"></div> where it should go.
To open it over the page from a button instead, add data-mode="popup" to the script tag, then:
<button data-mailyte-open="frm_8kq2xv9btn4lw0">Join the waitlist</button>Your page can react when someone signs up:
Mailyte.on('submitted', function (e) {
// e.formId, e.status: "subscribed" or "pending_confirmation"
});If you prefer a plain iframe, the Share tab has that too: https://mailyte.com/embed/f/<form id>.
It posts { type: "mailyte:resize", formId, height } to your page whenever its size changes.
3. Your own HTML
Full control of the styling, no script needed:
<form action="https://app.mailyte.com/api/v1/public/forms/frm_8kq2xv9btn4lw0/submissions" method="post">
<input type="email" name="email" required>
<input type="text" name="name">
<input type="hidden" name="campaign" value="launch-oct-2026">
<input type="text" name="website" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px">
<button>Join</button>
</form>After a signup the browser goes to the thank-you page you set on the form, or to ours. If the signup
is refused, it comes back to your page with ?mailyte_error=<reason> (the reasons are below).
Leave the website field in and empty. People never see it; bots fill it in, and we quietly drop
those signups.
Turn off the bot check for this form. On the form's Setup tab, Check that each person is not a bot uses Cloudflare Turnstile, which only works on our own pages (the link and the iframe). A form on your site cannot pass it. The honeypot, the limits below and the address check still protect it.
4. From your own code
const res = await fetch('https://app.mailyte.com/api/v1/public/forms/frm_8kq2xv9btn4lw0/submissions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
email: 'ada@example.com',
name: 'Ada Obi',
campaign: 'launch-oct-2026',
fields: { company: 'Acme Ltd' },
consent: true,
page_url: location.href,
}),
});| Field | Notes |
|---|---|
email |
Required. Up to 254 characters. |
name |
Optional, or required if the form says so. Up to 120 characters. |
campaign |
Optional label for the push that brought them, up to 100 characters. Taken from utm_campaign when you leave it out. |
fields |
Your contact fields. Only the ones the form asks for are accepted. metadata is accepted as another name for it. |
tags |
Optional. Only the tags listed under Tags your website may add on the form. |
consent |
Required when the form shows a consent box. We keep the exact text the person was shown. |
page_url, referrer, utm |
Optional. UTM tags in page_url are read too. |
website |
The honeypot. Leave it empty or out. |
What comes back
A signup the form accepts is always 202, with the same answer whether the address is new or
already on your list, so the form cannot be used to find out who your subscribers are:
{ "data": { "status": "pending_confirmation", "message": "Almost done. We sent you an email...", "redirect_url": null } }status is subscribed (they joined) or pending_confirmation (they must press the button in the
email we sent first).
A refusal carries an error_code and a message that is safe to show the person:
| Code | HTTP | Meaning |
|---|---|---|
invalid_email |
422 | Not written correctly, or a mistyped domain. data.suggestion may hold the fix, like ada@gmail.com: offer it, never apply it yourself. |
undeliverable_email |
422 | The domain cannot receive mail, or the mailbox is known not to exist. |
disposable_email |
422 | A temporary address. |
role_address |
422 | A shared address like info@ (only on forms set to Strict). |
consent_required |
422 | The consent box was not ticked. |
validation_failed, unknown_field, tag_not_allowed |
422 | Something else in the body; errors says what. |
origin_not_allowed |
403 | The page is not on the form's list of websites. |
captcha_failed |
403 | The bot check did not pass. |
form_closed |
410 | The form was closed. |
rate_limited |
429 | Too many tries. Wait Retry-After seconds. |
Confirming their email
Newsletter forms ask people to confirm by default. We email a button from the sender you chose; the
person joins your list only when they press it, on a page at mailyte.com/confirm. Opening the link
does nothing by itself, so email security scanners that open every link cannot confirm for them.
Until they confirm, nothing is added to your contacts: you see them on the form's Signups tab as Waiting to confirm. The link works for 48 hours.
Someone who unsubscribed before always has to confirm, whatever the form says. Pressing the button is how they come back, and it lifts their unsubscribe so your emails reach them again.
Websites that may post
On the Setup tab, Websites that may post to this form takes one site per line, like
https://acme.com. Empty means any website. It is a browser rule: a page on another site is refused,
but a script calling the address directly is not, so it does not replace the other protections.
Testing two versions
On any paid plan, the form's A/B test tab shows half your visitors (or the share you choose) a different title, description, button or colour. Each visitor always sees the same version. The tab shows views, signups and the rate for each, and says when one is ahead for real (95% sure, after at least 100 views each). Keep the winner and its words become the form's.
If you post to the form from your own code, nothing changes for you: a test only runs on our hosted page, the one-line script and the iframe. Signups from your code are not part of it.
Webhooks
Subscribe a webhook to any of these to feed your own system:
| Event | When |
|---|---|
form.submitted |
Every signup the form accepted, including people already on your list. |
contact.subscribed |
Someone joined through a form, at once or by confirming. |
contact.confirmed |
Someone pressed the button in the confirmation email. |
Each carries the form (id, name, type), the contact, the campaign, utm, fields and the
consent record. See Receiving webhooks for signing.
From your server instead
If the signup reaches your backend first, you do not need a form at all: add the person with
PUT /api/v1/contacts and an API key with the contacts:write scope, sending the list in list_ids
and the consent evidence in consented_at and consent_source. See
Checking contacts added through the API.