3 fields
Newsletter signup form
A newsletter signup form with an explicit consent checkbox. Ikiform validates and stores each signup, then hands it to your email tool by webhook.
Preview
Runs Ikiform's validator in your browser. Nothing is sent.
Copy the code
Every version below is generated from the same field definition. Replace frm_xxxxxxxxxxxxxxxx with your form id, or enter it in the Form ID box and the code updates.
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<label for="first_name">First name</label>
<input id="first_name" name="first_name" type="text" maxlength="80" />
<label><input name="consent" type="checkbox" required /> Email me the newsletter. I can unsubscribe at any time.</label>
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Subscribe</button>
</form>- HTML needs no JavaScript. After a successful post the browser is redirected to the form's redirect URL, or to Ikiform's hosted thank-you page.
- React uses the SDK:
npm install @ikiform/sdk. Field errors from the server appear under each field. - JavaScript enhances the HTML form with
fetch, so the visitor stays on the page. - The hidden
_gotchainput is the honeypot. Leave it empty and hidden.
challenge(FORM_ID) from @ikiform/sdk and pass it to submit as turnstileToken, or add it to the fetch body as cf-turnstile-response. Without it the post is refused with 403 challenge_required. See the Next.js guide and Spam protection.The fields
Paste this definition on the form's Fields tab in the dashboard. Ikiform then checks every post against it on the server, refuses bad data with 422 validation_failed, refuses any field that isn't listed, and stores typed values.
| Field id | Type | Required | Accepts |
|---|---|---|---|
email | email | Yes | One valid address |
first_name | text | No | Up to 80 characters |
consent | checkbox | Yes | Must be checked, stored as true |
{
"version": 1,
"title": "Newsletter",
"fields": [
{
"id": "email",
"type": "email",
"label": "Email",
"required": true
},
{
"id": "first_name",
"type": "text",
"label": "First name",
"validation": {
"max_length": 80
}
},
{
"id": "consent",
"type": "checkbox",
"label": "Email me the newsletter. I can unsubscribe at any time.",
"required": true
}
],
"settings": {
"submit_label": "Subscribe",
"success_message": "Thanks for subscribing."
}
}Next.js server action
To keep the form working without JavaScript in a Next.js app, post from a server action with a secret key. Create the key under API keys with the submissions:write scope and keep it out of the browser.
IKIFORM_SECRET_KEY=ik_sk_your_secret_key"use server";
export interface NewsletterFormState {
ok: boolean;
message: string;
fields?: Record<string, string[]>;
values?: Record<string, string[]>;
}
export async function sendNewsletterForm(_previous: NewsletterFormState, formData: FormData): Promise<NewsletterFormState> {
const body = new URLSearchParams();
for (const [name, value] of formData) {
if (typeof value === "string" && !name.startsWith("$ACTION")) body.append(name, value);
}
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
},
body,
});
const result = await response.json();
if (result.ok) return { ok: true, message: "Thanks for subscribing." };
const values = Object.fromEntries([...new Set(body.keys())].map((name) => [name, body.getAll(name)]));
return { ok: false, message: result.error.message, fields: result.error.fields, values };
}"use client";
import { useActionState } from "react";
import { type NewsletterFormState, sendNewsletterForm } from "./actions";
const initial: NewsletterFormState = { ok: false, message: "" };
export function NewsletterForm() {
const [state, action, pending] = useActionState(sendNewsletterForm, initial);
if (state.ok) return <p>{state.message}</p>;
return (
<form action={action}>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" autoComplete="email" required defaultValue={state.values?.email?.[0]} />
{state.fields?.email ? <p>{state.fields.email[0]}</p> : null}
<label htmlFor="first_name">First name</label>
<input id="first_name" name="first_name" type="text" maxLength={80} defaultValue={state.values?.first_name?.[0]} />
{state.fields?.first_name ? <p>{state.fields.first_name[0]}</p> : null}
<label><input name="consent" type="checkbox" required defaultChecked={Boolean(state.values?.consent)} /> Email me the newsletter. I can unsubscribe at any time.</label>
{state.fields?.consent ? <p>{state.fields.consent[0]}</p> : null}
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{state.message && !state.fields ? <p role="alert">{state.message}</p> : null}
<button disabled={pending}>{pending ? "Sending" : "Subscribe"}</button>
</form>
);
}Set it up
- Create a form in the dashboard and copy its id.
- Paste the field definition on the Fields tab.
- Add your site under Allowed domains, plus
localhostwhile you develop. - Set the rate limit to Standard, 5 a minute per visitor if visitors post from the browser. A signup is one post. Standard stops a script from adding addresses in bulk. With the server action, every post comes from your server's IP, so choose Relaxed or Custom instead.
- Paste the code, send a test and check it arrives in the dashboard.
Recording consent
A checkbox field with required: true must be checked, or the post is refused with This box must be checked. The stored value is true, and each submission keeps its timestamp, origin and referrer, which together record where and when the person agreed.
Write the label as the exact promise you are making. If you change the wording later, older submissions still record that they agreed, but not to which text, so note the date you changed it.
Ikiform is the intake, not the list
Ikiform doesn't email the person who signs up. Notification emails go to your workspace, not to the subscriber, so there is no built-in confirmation or double opt-in email. Ikiform also doesn't send newsletters or handle unsubscribes. Forward each signup by webhook to the email tool that does, and let it send the confirmation for double opt-in.
| Step | Where it happens |
|---|---|
| Validate the address and consent, filter spam | Ikiform |
| Store the signup with its timestamp | Ikiform |
| Add to the list, confirm, unsubscribe | Your email tool, through a webhook |
One form, many pages
Newsletter forms often sit in a footer on every page. Each submission's metadata includes the referrer, so you can see which page the signup came from without adding a hidden field. Add every domain the form appears on under Allowed domains.
email for the field id. A disposable address adds 2 to the spam score, which on its own is not enough to mark a signup as spam.Where to send it
Every submission can go to a signed webhook. These guides cover the tools that suit this form, with the relay code:
- Send form submissions to Zapier
- Send form submissions to Make
- Send form submissions to n8n
- Send form submissions to Pipedream
- Send form submissions to Google Sheets
Questions
No. Ikiform collects, validates and stores signups and delivers them by webhook. Sending, confirmation emails and unsubscribes belong in your email tool.
Make the consent checkbox required. Ikiform refuses posts where it isn't checked and stores true with the submission's timestamp, origin and referrer.
Yes. Add each domain under Allowed domains, up to 20 per form. Subdomains of a listed domain are allowed automatically.