뉴스레터 가입 폼

뉴스레터 가입 폼4분 분량

내 뉴스레터 폼의 가입을 MailerLite로 보내기

내 가입 폼을 MailerLite에 연결합니다. 릴레이가 새 독자를 더블 옵트인을 위해 미확인으로 추가하고, 구독자에게 두 번 확인을 요청하지 않습니다.

Preet SutharIkiform 창업자

내 폼을 Ikiform으로 POST하고, MailerLite API를 호출하는 릴레이로 웹훅을 보내세요. 릴레이는 먼저 주소를 조회합니다. 새 독자는 unconfirmed로 만들어지며, API 설정이 켜져 있으면 MailerLite가 더블 옵트인 이메일을 보냅니다. 기존 구독자는 새 그룹에만 추가됩니다. 이 조회 덕분에 확인된 구독자가 다시 확인을 요청받지 않습니다.

MailerLite에서 다른 점

MailerLite의 구독자 생성 엔드포인트는 upsert입니다. /api/subscribers로 POST 한 번이면 새 구독자를 만들거나 기존 구독자를 업데이트하고, 201이나 200으로 응답합니다. 비파괴적이라서 빼먹은 그룹과 필드는 그대로 남습니다. 문제는 status입니다. 계정 설정 Double opt-in for API and integrations(API 및 연동용 더블 옵트인)가 켜져 있으면 MailerLite는 새 API 구독자에게 확인 이메일을 보냅니다. 기존의 활성 또는 수신 거부 구독자는 호출이 status: "unconfirmed"를 보낼 때만 확인 이메일을 받습니다.

MailerLite 도움말에 따르면 기존의 활성 또는 수신 거부 구독자는 호출에 "status": "unconfirmed"가 없으면 확인 이메일을 받지 않습니다. 그래서 항상 unconfirmed를 보내는 릴레이는 충성 독자의 반복 가입을 모두 새 확인 요청으로 바꿉니다. 해결책은 무엇을 보낼지 정하기 전에 주소를 조회하는 것입니다.

릴레이가 MailerLite에 무엇을 보낼지 정하는 방식Ikiform 웹훅서명 검증됨GET 구독자이메일로404: 신규미확인으로 생성active그룹만 추가unsubscribed그대로 둠못 찾음찾음릴레이가 MailerLite에 무엇을 보낼지 정하는 방식Ikiform 웹훅서명 검증됨GET 구독자이메일로404: 신규미확인으로 생성active그룹만 추가unsubscribed그대로 둠못 찾음찾음
  1. 릴레이가 Ikiform의 웹훅을 검증합니다.
  2. MailerLite에 이메일로 구독자를 요청합니다.
  3. 없음: 상태 unconfirmed와 위치의 그룹으로 만들고, MailerLite가 확인 이메일을 보냅니다.
  4. 있고 active나 unconfirmed: 상태 없이 그룹만 추가하므로 확인 상태는 바뀌지 않습니다.
  5. 있고 unsubscribed: 아무것도 하지 않으므로 가입이 수신 거부를 몰래 되돌리지 않습니다.
가입마다 API 호출 두 번이며, 두 번째 호출은 첫 번째가 찾은 결과에 따라 달라집니다.

MailerLite 설정하기

  1. MailerLite에서 구독 설정을 열고 Double opt-in for API and integrations를 켜세요. 확인 이메일은 거기서 편집합니다. MailerLite는 API로 추가된 모든 구독자에게 같은 이메일을 보냅니다.
  2. "Website footer"와 "Blog posts"처럼 위치마다 그룹을 만들고 각 그룹의 ID를 기록하세요.
  3. Integrations에서 API 토큰을 만들고 릴레이의 환경 변수용으로 보관하세요.
MailerLite 요금 FAQ에 따르면 가장 낮은 플랜에서는 API 접근이 제한됩니다. 의존하기 전에 내 플랜에서 토큰으로 구독자를 만들 수 있는지 테스트하세요.

릴레이

이것은 Express 서버입니다. 서명은 Ikiform이 보낸 정확한 바이트를 대상으로 하고 JSON을 먼저 파싱하면 바이트가 바뀌므로, 원시 본문을 텍스트로 읽습니다.

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는 토큰을 Authorization: Bearer로 받습니다. 기본 URL은 https://connect.mailerlite.com/api입니다.
  • GET /api/subscribers/{email}은 이메일이나 ID를 받고, 구독자가 없으면 404로 응답합니다.
  • groups는 이미 존재하는 그룹 ID를 받습니다. 틀린 ID는 422로 거부되며, 릴레이는 이를 MailerLite의 메시지와 함께 502로 전달합니다.
  • fields.name은 MailerLite의 기본 이름 필드입니다. 커스텀 필드는 보내기 전에 MailerLite에 있어야 합니다.
  • 208은 수신 거부한 독자를 위한 내부 표시입니다. Ikiform에는 200과 전달 로그의 메모로 보입니다.

443 포트의 공개 HTTPS URL을 주는 곳이라면 어디든 배포하고, Ikiform의 Webhooks에 그 URL을 추가하고, whsec_ 시크릿을 IKIFORM_WEBHOOK_SECRET에 넣으세요. 재시도와 페이로드는 웹훅 문서에서 다루고, 자체 서버에는 데이터베이스를 쓰는 같은 Express 구성이 있습니다.

MailerLite의 생성 호출은 subscribed_at, ip_address, opted_in_at, optin_ip를 받습니다. IP 필드는 빼세요. Ikiform의 웹훅에는 방문자의 IP 주소나 그 해시가 절대 포함되지 않으므로 릴레이가 보낼 실제 값이 없고, 지어낸 값은 없는 것보다 나쁩니다.

가입 기록은 Ikiform에 남습니다. 제출에 타임스탬프, origin, referrer, 국가와 함께 consent: true가 보관됩니다. 확인 기록은 독자가 링크를 클릭한 시점을 기록하는 MailerLite에 있습니다. 둘을 합치면 누군가 언제 체크박스를 선택했고 언제 주소가 자기 것임을 증명했는지 보여 줄 수 있습니다. 필요할 때 Submissions에서 Ikiform 쪽을 CSV로 내보내세요.

레이트 리밋

MailerLite의 전역 API 한도는 분당 120개 요청입니다. 릴레이는 가입마다 두 번 호출하므로 분당 약 60건이 들어갑니다. 그 이상이면 MailerLite가 429로, 릴레이가 Retry-After와 함께 503으로 응답하고, Ikiform이 나중에 같은 전달로 다시 시도합니다.

MailerLite 응답릴레이 응답Ikiform
201 또는 200204전달 성공
429Retry-After와 함께 503기다린 뒤 재시도
401502재시도. 토큰을 고친 뒤 실패한 전달에서 Retry now를 쓰세요
422오류와 함께 502재시도. 대개 그룹 ID나 필드 이름이 틀린 경우입니다

주의할 점

  • status: "active"를 보내지 마세요. 그 사람을 구독 상태로 표시하며, 그래도 확인 이메일이 나가는지는 MailerLite 문서에 나와 있지 않습니다. 보내는 상태로 테스트하세요.
  • 재구독은 선택입니다. MailerLite에는 수신 거부한 사람을 위한 resubscribe 플래그가 있습니다. 폼이 그렇다고 명시하고 독자가 다시 동의한다는 것을 알 때만 보내세요.
  • 토글이 중요합니다. MailerLite는 Double opt-in for API and integrations가 켜져 있을 때만 API 가입에 확인 이메일을 보냅니다.
  • 확인 이메일은 하나. API 더블 옵트인 이메일은 모든 API 가입이 공유합니다. 위치마다 문구가 달라야 한다면 대신 별도의 MailerLite 폼을 쓰세요.
  • Ikiform의 중복. Ikiform은 받아들인 모든 POST를 보관하고, MailerLite는 이메일당 구독자 하나를 보관합니다. 구독자 수는 Ikiform 대시보드가 아니라 MailerLite에서 세세요.

작동 확인하기

  1. Ikiform의 엔드포인트에서 테스트를 보내세요. 전달에 204가 표시됩니다.
  2. 내 새 주소로 구독하세요. MailerLite에 푸터 그룹의 미확인 구독자로 표시되고 확인 이메일이 도착합니다.
  3. 확인한 뒤 블로그 글에서 다시 구독하세요. 구독자는 활성으로 유지되고, 두 번째 그룹에 들어가며, 새 확인 이메일은 받지 않습니다.
  4. MailerLite에서 그 주소를 수신 거부하고 한 번 더 구독하세요. 전달 로그에 수신 거부 메모가 표시되고, MailerLite에서는 여전히 수신 거부 상태입니다.

질문

막히는 단계가 있거나 내 상황에 맞는 가이드가 필요하다면 이메일을 보내세요: hello@ikiform.com.