Forms

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.

Form ID

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 _gotcha input is the honeypot. Leave it empty and hidden.
If you set the form's spam check to Ikiform, the HTML form keeps working with no change and the server action skips the check because it sends a secret key. The React and JavaScript versions must get a token before each submit: call 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 idTypeRequiredAccepts
emailemailYesOne valid address
first_nametextNoUp to 80 characters
consentcheckboxYesMust be checked, stored as true
Field definition
{
  "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.

.env.local
IKIFORM_SECRET_KEY=ik_sk_your_secret_key
app/newsletter-signup/actions.ts
"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 };
}
app/newsletter-signup/form.tsx
"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>
  );
}
A server action posts from your server's IP, so the per-visitor rate limit counts every visitor together. Use the Relaxed or Custom rate limit for forms you send through a server.

Set it up

  1. Create a form in the dashboard and copy its id.
  2. Paste the field definition on the Fields tab.
  3. Add your site under Allowed domains, plus localhost while you develop.
  4. 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.
  5. Paste the code, send a test and check it arrives in the dashboard.

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.

StepWhere it happens
Validate the address and consent, filter spamIkiform
Store the signup with its timestampIkiform
Add to the list, confirm, unsubscribeYour 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.

Use 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:

Questions

Point your form at Ikiform