Newsletter signup form6 min read
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.
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:
- The relay verifies Ikiform's webhook.
- It asks MailerLite for the subscriber by email.
- Not found: it creates them with status unconfirmed and the placement's group, and MailerLite sends the confirmation email.
- Found and active or unconfirmed: it adds the group without a status, so nothing about their confirmation changes.
- Found and unsubscribed: it does nothing, so a signup never silently undoes an unsubscribe.
Set up MailerLite
- 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.
- Create a group per placement, such as "Website footer" and "Blog posts", and note each group's id.
- Under Integrations, create an API token and keep it for the relay's environment.
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.
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 ishttps://connect.mailerlite.com/api. GET /api/subscribers/{email}accepts an email or an id and answers404when the subscriber doesn't exist.groupstakes group ids that already exist. A wrong id is refused with422, which the relay passes on as502with MailerLite's message.fields.nameis MailerLite's default first name field. Custom fields must exist in MailerLite before you send them.- The
208is an internal marker for an unsubscribed reader. Ikiform sees200and 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 cover retries and the payload, and your own server 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
resubscribeflag 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
- Send a test from the endpoint in Ikiform. The delivery shows
204. - Subscribe with a new address you own. MailerLite shows the subscriber as unconfirmed in the footer group, and the confirmation email arrives.
- Confirm it, then subscribe again from a blog post. The subscriber stays active, joins the second group, and gets no new confirmation email.
- Unsubscribe that address in MailerLite and subscribe once more. The delivery log shows the unsubscribed note, and MailerLite still shows them as unsubscribed.
Questions
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.
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.
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.
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.
No. Ikiform never emails the person who submits a form. MailerLite sends the confirmation, every newsletter and the unsubscribe link.
Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.