# Send your own newsletter form's signups to MailerLite

> Connect your own signup form to MailerLite: a relay adds new readers as unconfirmed for double opt-in and never asks a subscriber to confirm twice.

Published 2026-10-25 · https://ikiform.com/guides/newsletter-signup/mailerlite

Post your own form to Ikiform and point the webhook at a relay that calls MailerLite's API. The relay looks the address up first: a new reader is created as `unconfirmed`, which makes MailerLite send its double opt-in email when the API setting is on, and an existing subscriber only gets the new group. That lookup is what stops a confirmed subscriber from being asked to confirm again.

## What's different with MailerLite

MailerLite's create subscriber endpoint is an upsert: one `POST` to `/api/subscribers` creates a new subscriber or updates an existing one, answering `201` or `200`. It's non-destructive, so groups and fields you leave out stay as they were. The catch is `status`. With the account setting **Double opt-in for API and integrations** on, MailerLite emails new API subscribers a confirmation. An existing active or unsubscribed subscriber only gets one when the call sends `status: "unconfirmed"`.

MailerLite's help says existing active or unsubscribed subscribers don't get the confirmation email unless the call includes `"status": "unconfirmed"`. So a relay that always sends `unconfirmed` turns every repeat signup from a loyal reader into a fresh request to confirm. The fix is to look the address up before deciding what to send:

Diagram: How the relay decides what to send MailerLite

1. The relay verifies Ikiform's webhook.
2. It asks MailerLite for the subscriber by email.
3. Not found: it creates them with status unconfirmed and the placement's group, and MailerLite sends the confirmation email.
4. Found and active or unconfirmed: it adds the group without a status, so nothing about their confirmation changes.
5. Found and unsubscribed: it does nothing, so a signup never silently undoes an unsubscribe.

Two API calls per signup, and the second one depends on what the first one found.

## Set up MailerLite

1. In MailerLite, open the subscribe settings and turn on **Double opt-in for API and integrations**. Edit the confirmation email there; MailerLite sends the same one to every subscriber added through the API.
2. Create a group per placement, such as "Website footer" and "Blog posts", and note each group's id.
3. Under **Integrations**, create an API token and keep it for the relay's environment.

> MailerLite's pricing FAQ says API access is limited on its lowest plan. Test that the token can create subscribers on your plan before you rely on it.

## The relay

This is an Express server. It reads the raw body as text, because the signature covers the exact bytes Ikiform sent, and parsing JSON first would change them.

`server.ts`:

```ts
import express from "express";
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

const API = "https://connect.mailerlite.com/api";
const GROUPS: Record<string, string> = {
  footer: process.env.MAILERLITE_GROUP_FOOTER!,
  post: process.env.MAILERLITE_GROUP_POST!,
};

function mailerlite(path: string, init: RequestInit = {}) {
  return fetch(`${API}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.MAILERLITE_API_KEY}`,
      "Content-Type": "application/json",
      Accept: "application/json",
    },
  });
}

async function subscribe(email: string, name: string | undefined, source: string) {
  const found = await mailerlite(`/subscribers/${encodeURIComponent(email)}`);
  if (found.status !== 200 && found.status !== 404) return found;

  const groups = [GROUPS[source] ?? process.env.MAILERLITE_GROUP_FOOTER!];
  const fields = name ? { name } : {};

  if (found.status === 404) {
    return mailerlite("/subscribers", {
      method: "POST",
      body: JSON.stringify({ email, fields, groups, status: "unconfirmed" }),
    });
  }

  const existing = await found.json();
  if (existing.data.status === "unsubscribed") return new Response(null, { status: 208 });
  return mailerlite("/subscribers", { method: "POST", body: JSON.stringify({ email, fields, groups }) });
}

const app = express();

app.post("/hooks/mailerlite", express.text({ type: "application/json", limit: "512kb" }), async (req, res) => {
  let event;
  try {
    event = await verifyWebhook({ payload: req.body, headers: req.headers, secret: process.env.IKIFORM_WEBHOOK_SECRET! });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return res.status(400).send(error.reason);
    return res.status(500).end();
  }
  if (event.type !== "submission.created") return res.status(204).end();

  const { email, first_name, consent, source } = event.data.fields;
  if (consent !== true || typeof email !== "string") return res.status(200).send("Skipped");

  const answer = await subscribe(
    email,
    typeof first_name === "string" ? first_name : undefined,
    typeof source === "string" ? source : "footer",
  );
  if (answer.status === 208) return res.status(200).send("Unsubscribed earlier, left alone");
  if (answer.status === 429) {
    return res.status(503).set("retry-after", answer.headers.get("retry-after") ?? "60").send("MailerLite rate limit");
  }
  if (!answer.ok) return res.status(502).send(`MailerLite answered ${answer.status}: ${await answer.text()}`);
  res.status(204).end();
});

app.listen(Number(process.env.PORT ?? 3000));
```

- MailerLite takes the token as `Authorization: Bearer`. The base URL is `https://connect.mailerlite.com/api`.
- `GET /api/subscribers/{email}` accepts an email or an id and answers `404` when the subscriber doesn't exist.
- `groups` takes group ids that already exist. A wrong id is refused with `422`, which the relay passes on as `502` with MailerLite's message.
- `fields.name` is MailerLite's default first name field. Custom fields must exist in MailerLite before you send them.
- The `208` is an internal marker for an unsubscribed reader. Ikiform sees `200` and a note in the delivery log.

Deploy it anywhere that gives you a public HTTPS URL on port 443, add that URL under **Webhooks** in Ikiform, and put the `whsec_` secret in `IKIFORM_WEBHOOK_SECRET`. The [webhooks docs](https://ikiform.com/docs/webhooks.md) cover retries and the payload, and [your own server](https://ikiform.com/blog/form-submissions-to-your-own-server.md) has the same Express setup with a database.

## Where the consent record lives

MailerLite's create call accepts `subscribed_at`, `ip_address`, `opted_in_at` and `optin_ip`. Leave the IP fields out. Ikiform's webhook never includes the visitor's IP address or its hash, so the relay has nothing true to send, and an invented value is worse than none.

The record of the signup stays in Ikiform: the submission keeps `consent: true` with its timestamp, origin, referrer and country. The record of the confirmation lives in MailerLite, which notes when the reader clicked the link. Between the two you can show when someone ticked the box and when they proved the address was theirs. Export the Ikiform side as CSV from **Submissions** when you need it.

## Rate limits

MailerLite's global API limit is 120 requests per minute. The relay makes two per signup, so about 60 signups a minute fit. Over that, MailerLite answers `429`, the relay answers `503` with `Retry-After`, and Ikiform tries again later with the same delivery.

| MailerLite answers | Relay answers | Ikiform |
| --- | --- | --- |
| `201` or `200` | `204` | Delivery succeeded |
| `429` | `503` with `Retry-After` | Waits, then retries |
| `401` | `502` | Retries. Fix the token, then use **Retry now** on failed deliveries |
| `422` | `502` with the errors | Retries. Usually a wrong group id or field name |

## Gotchas

- **Don't send `status: "active"`.** It marks the person as subscribed, and MailerLite's docs don't say whether a confirmation still goes out. Test with the status you send.
- **Resubscribing is a choice.** MailerLite has a `resubscribe` flag for people who unsubscribed. Only send it when your form says so, and when the reader knows they're opting back in.
- **The toggle matters.** MailerLite only sends API signups a confirmation email when **Double opt-in for API and integrations** is on.
- **One confirmation email.** The API double opt-in email is shared by every API signup. If you need different wording per placement, use separate MailerLite forms instead.
- **Duplicates in Ikiform.** Ikiform keeps every accepted post, while MailerLite keeps one subscriber per email. Count subscribers in MailerLite, not in the Ikiform dashboard.

## Check it works

1. Send a test from the endpoint in Ikiform. The delivery shows `204`.
2. Subscribe with a new address you own. MailerLite shows the subscriber as unconfirmed in the footer group, and the confirmation email arrives.
3. Confirm it, then subscribe again from a blog post. The subscriber stays active, joins the second group, and gets no new confirmation email.
4. Unsubscribe that address in MailerLite and subscribe once more. The delivery log shows the unsubscribed note, and MailerLite still shows them as unsubscribed.

## Questions

### Can I use my own HTML form with MailerLite instead of its embed?

Yes. Post the form to Ikiform, and let a relay add each signup through MailerLite's API with the right group. The form stays in your markup and MailerLite still sends the newsletter.

### Why doesn't MailerLite send a confirmation email to API signups?

Either Double opt-in for API and integrations is off in MailerLite's subscribe settings, or the relay created the subscriber as active. Keep the setting on, send status unconfirmed, and test with an address you own.

### Will a subscriber who signs up twice get two confirmation emails?

Not with this relay. It looks the address up first and only sends unconfirmed for a new subscriber. An existing subscriber is added to the new group and nothing else changes.

### How do I put footer and blog signups in different MailerLite groups?

Give each placement a different value in the form's hidden source input, then map each value to a MailerLite group id in the relay. An existing subscriber who signs up from a second placement joins that group too, and keeps the first.

### Does Ikiform send anything to the subscriber?

No. Ikiform never emails the person who submits a form. MailerLite sends the confirmation, every newsletter and the unsubscribe link.

## More guides

- [Newsletter signup form](https://ikiform.com/guides/newsletter-signup.md): Build a newsletter signup form in your own markup, record consent, block bot signups and pass each subscriber to Kit, MailerLite, beehiiv or any tool.
- [Double opt-in](https://ikiform.com/guides/newsletter-signup/double-opt-in.md): Ikiform doesn't send confirmation emails, so double opt-in runs in your email tool: a relay adds each signup as unconfirmed and the tool sends the link.
- [Kit (ConvertKit)](https://ikiform.com/guides/newsletter-signup/kit.md): Connect your own newsletter form to Kit with a relay that creates the subscriber, adds them to a Kit form for the incentive email, and tags the placement.
- [beehiiv](https://ikiform.com/guides/newsletter-signup/beehiiv.md): Connect your own signup form to beehiiv: a relay creates each subscription with double opt-in on, the placement as UTM source and the page as referrer.
- [Static site](https://ikiform.com/guides/newsletter-signup/static-site.md): Add a newsletter signup form to a Hugo, Jekyll, Eleventy or Astro static site, keep the email tool's API key out of the page and confirm in the tool.
- [Next.js newsletter form](https://ikiform.com/guides/newsletter-signup/nextjs.md): A Next.js footer newsletter form: a server action with a consent check and Turnstile, posting to Ikiform, plus a route handler that adds readers to Kit.
