Newsletter-Anmeldeformular

Newsletter-Anmeldeformular6 Min. Lesezeit

Anmeldungen aus deinem eigenen Newsletter-Formular an MailerLite senden

Verbinde dein eigenes Anmeldeformular mit MailerLite: Ein Relay fügt neue Leser für das Double Opt-in unbestätigt hinzu und bittet einen Abonnenten nie zweimal um Bestätigung.

Preet SutharGründer, Ikiform

Sende dein eigenes Formular an Ikiform und richte den Webhook auf ein Relay aus, das die API von MailerLite aufruft. Das Relay schlägt die Adresse zuerst nach: Ein neuer Leser wird als unconfirmed angelegt, was MailerLite bei aktivierter API-Einstellung seine Double-Opt-in-E-Mail senden lässt, und ein bestehender Abonnent bekommt nur die neue Gruppe. Dieses Nachschlagen verhindert, dass ein bestätigter Abonnent erneut um Bestätigung gebeten wird.

Was bei MailerLite anders ist

Der Endpunkt zum Anlegen von Abonnenten bei MailerLite ist ein Upsert: Ein POST an /api/subscribers legt einen neuen Abonnenten an oder aktualisiert einen bestehenden und antwortet mit 201 oder 200. Er ist nicht destruktiv, Gruppen und Felder, die du weglässt, bleiben also, wie sie waren. Der Haken ist status. Mit aktivierter Kontoeinstellung Double opt-in for API and integrations schickt MailerLite neuen API-Abonnenten eine Bestätigung. Ein bestehender aktiver oder abgemeldeter Abonnent bekommt nur eine, wenn der Aufruf status: "unconfirmed" sendet.

Laut der Hilfe von MailerLite bekommen bestehende aktive oder abgemeldete Abonnenten die Bestätigungs-E-Mail nicht, außer der Aufruf enthält "status": "unconfirmed". Ein Relay, das immer unconfirmed sendet, macht also aus jeder wiederholten Anmeldung eines treuen Lesers eine neue Bitte um Bestätigung. Die Lösung ist, die Adresse nachzuschlagen, bevor entschieden wird, was gesendet wird:

Wie das Relay entscheidet, was es an MailerLite sendetIkiform-WebhookSignatur verifiziertGET Abonnentper E-Mail404: neuals unconfirmedanlegenaktivnur die Gruppehinzufügenabgemeldetin Ruhe lassennicht gefundengefundenWie das Relay entscheidet, was es an MailerLite sendetIkiform-WebhookSignatur verifiziertGET Abonnentper E-Mail404: neuals unconfirmedanlegenaktivnur dieGruppehinzufügenabgemeldetin Ruhelassennicht gefundengefunden
  1. Das Relay verifiziert den Webhook von Ikiform.
  2. Es fragt MailerLite per E-Mail nach dem Abonnenten.
  3. Nicht gefunden: Es legt ihn mit Status unconfirmed und der Gruppe der Platzierung an, und MailerLite sendet die Bestätigungs-E-Mail.
  4. Gefunden und active oder unconfirmed: Es fügt die Gruppe ohne Status hinzu, sodass sich an der Bestätigung nichts ändert.
  5. Gefunden und unsubscribed: Es tut nichts, damit eine Anmeldung nie stillschweigend eine Abmeldung rückgängig macht.
Zwei API-Aufrufe pro Anmeldung, und der zweite hängt davon ab, was der erste gefunden hat.

MailerLite einrichten

  1. Öffne in MailerLite die Abo-Einstellungen und aktiviere Double opt-in for API and integrations. Bearbeite dort die Bestätigungs-E-Mail; MailerLite sendet dieselbe an jeden über die API hinzugefügten Abonnenten.
  2. Lege pro Platzierung eine Gruppe an, etwa „Website footer“ und „Blog posts“, und notiere die ID jeder Gruppe.
  3. Erstelle unter Integrations ein API-Token und bewahre es für die Umgebung des Relays auf.
Laut den Preis-FAQ von MailerLite ist der API-Zugang im günstigsten Tarif eingeschränkt. Teste, dass das Token in deinem Tarif Abonnenten anlegen kann, bevor du dich darauf verlässt.

Das Relay

Das ist ein Express-Server. Er liest den rohen Body als Text, weil die Signatur genau die Bytes abdeckt, die Ikiform gesendet hat, und vorheriges JSON-Parsen würde sie verändern.

server.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 nimmt das Token als Authorization: Bearer. Die Basis-URL ist https://connect.mailerlite.com/api.
  • GET /api/subscribers/{email} akzeptiert eine E-Mail oder eine ID und antwortet mit 404, wenn der Abonnent nicht existiert.
  • groups nimmt IDs bereits existierender Gruppen. Eine falsche ID wird mit 422 abgelehnt, was das Relay als 502 mit der Meldung von MailerLite weitergibt.
  • fields.name ist das Standardfeld für den Vornamen bei MailerLite. Benutzerdefinierte Felder müssen in MailerLite existieren, bevor du sie sendest.
  • Der 208 ist eine interne Markierung für einen abgemeldeten Leser. Ikiform sieht 200 und einen Hinweis im Zustellungsprotokoll.

Deploye ihn überall, wo du eine öffentliche HTTPS-URL auf Port 443 bekommst, füge diese URL unter Webhooks in Ikiform hinzu und leg das whsec_-Secret in IKIFORM_WEBHOOK_SECRET ab. Die Webhook-Doku behandelt Wiederholungen und den Payload, und dein eigener Server hat dasselbe Express-Setup mit einer Datenbank.

Der Create-Aufruf von MailerLite akzeptiert subscribed_at, ip_address, opted_in_at und optin_ip. Lass die IP-Felder weg. Der Webhook von Ikiform enthält nie die IP-Adresse des Besuchers oder ihren Hash, also hat das Relay nichts Wahres zu senden, und ein erfundener Wert ist schlimmer als keiner.

Der Nachweis der Anmeldung bleibt in Ikiform: Die Einsendung behält consent: true mit Zeitstempel, Origin, Referrer und Land. Der Nachweis der Bestätigung liegt in MailerLite, das festhält, wann der Leser auf den Link geklickt hat. Zusammen kannst du zeigen, wann jemand das Häkchen gesetzt und wann er bewiesen hat, dass die Adresse ihm gehört. Exportiere die Seite von Ikiform bei Bedarf als CSV aus Submissions.

Rate-Limits

Das globale API-Limit von MailerLite liegt bei 120 Requests pro Minute. Das Relay macht zwei pro Anmeldung, also passen etwa 60 Anmeldungen pro Minute. Darüber antwortet MailerLite mit 429, das Relay mit 503 und Retry-After, und Ikiform versucht es später mit derselben Zustellung erneut.

MailerLite antwortetRelay antwortetIkiform
201 oder 200204Zustellung erfolgreich
429503 mit Retry-AfterWartet, dann neuer Versuch
401502Neue Versuche. Korrigiere das Token und nutze dann Retry now bei fehlgeschlagenen Zustellungen
422502 mit den FehlernNeue Versuche. Meist eine falsche Gruppen-ID oder ein falscher Feldname

Stolperfallen

  • Sende nicht status: "active". Es markiert die Person als angemeldet, und die Doku von MailerLite sagt nicht, ob dann noch eine Bestätigung rausgeht. Teste mit dem Status, den du sendest.
  • Erneutes Anmelden ist eine Entscheidung. MailerLite hat ein resubscribe-Flag für Leute, die sich abgemeldet haben. Sende es nur, wenn dein Formular es sagt und der Leser weiß, dass er sich wieder einträgt.
  • Der Schalter zählt. MailerLite schickt API-Anmeldungen nur dann eine Bestätigungs-E-Mail, wenn Double opt-in for API and integrations aktiviert ist.
  • Eine Bestätigungs-E-Mail. Die Double-Opt-in-E-Mail der API teilen sich alle API-Anmeldungen. Brauchst du pro Platzierung einen anderen Text, nutze stattdessen separate MailerLite-Formulare.
  • Duplikate in Ikiform. Ikiform behält jeden angenommenen Post, während MailerLite einen Abonnenten pro E-Mail führt. Zähle Abonnenten in MailerLite, nicht im Ikiform-Dashboard.

Prüfen, ob es funktioniert

  1. Sende einen Test vom Endpunkt in Ikiform. Die Zustellung zeigt 204.
  2. Abonniere mit einer neuen Adresse, die dir gehört. MailerLite zeigt den Abonnenten unbestätigt in der Footer-Gruppe, und die Bestätigungs-E-Mail kommt an.
  3. Bestätige sie und abonniere dann erneut aus einem Blogbeitrag. Der Abonnent bleibt aktiv, kommt in die zweite Gruppe und bekommt keine neue Bestätigungs-E-Mail.
  4. Melde diese Adresse in MailerLite ab und abonniere noch einmal. Das Zustellungsprotokoll zeigt den Hinweis zur Abmeldung, und MailerLite zeigt sie weiterhin als abgemeldet.

Fragen

Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.