Skip to main content
Business & GrowthNewsletter Marketing Services300 lines

MailerLite

"MailerLite: email marketing platform API, subscriber groups, campaigns, automations, forms, analytics, REST API v2"

Quick Summary11 lines
You are an expert in integrating MailerLite for email marketing and newsletters.

## Key Points

- **Use upsert for subscriber creation**: The POST `/subscribers` endpoint is idempotent. It creates or updates, so you never need to check existence first.
- **Leverage cursor-based pagination**: MailerLite v2 uses cursor pagination, not page numbers. Always follow the `next_cursor` rather than guessing offsets.
- **Group subscribers at creation time**: Pass group IDs in the subscriber creation call to avoid a second API request for group assignment.
- **Exceeding rate limits during bulk imports**: The 120 req/min limit is strict. Batch subscribers using the bulk endpoint or add delays between individual calls.
- **Sending HTML without testing**: MailerLite strips certain HTML attributes for security. Always preview campaigns before scheduling to verify rendering.
skilldb get newsletter-marketing-services-skills/MailerLiteFull skill: 300 lines
Paste into your CLAUDE.md or agent config

MailerLite — Newsletter & Email Marketing

You are an expert in integrating MailerLite for email marketing and newsletters.

Core Philosophy

MailerLite is a lightweight yet full-featured email marketing platform. Its REST API (v2) lives at https://connect.mailerlite.com/api/. Authentication uses a Bearer token. Subscribers are organized into groups (not tags), and campaigns can target groups or segments. The API enforces rate limits of 120 requests per minute. Design integrations that batch subscriber operations, use groups for segmentation, and leverage webhooks for real-time event processing.

Setup & Configuration

Authentication and Client Setup

interface MailerLiteConfig {
  apiKey: string;
  baseUrl?: string;
}

class MailerLiteClient {
  private readonly baseUrl: string;
  private readonly headers: Record<string, string>;

  constructor(config: MailerLiteConfig) {
    this.baseUrl = config.baseUrl ?? "https://connect.mailerlite.com/api";
    this.headers = {
      Authorization: `Bearer ${config.apiKey}`,
      "Content-Type": "application/json",
      Accept: "application/json",
    };
  }

  async request<T>(
    method: string,
    path: string,
    body?: unknown
  ): Promise<T> {
    const url = `${this.baseUrl}${path}`;
    const res = await fetch(url, {
      method,
      headers: this.headers,
      body: body ? JSON.stringify(body) : undefined,
    });

    if (!res.ok) {
      const error = await res.json().catch(() => ({}));
      throw new Error(
        `MailerLite API ${res.status}: ${JSON.stringify(error)}`
      );
    }
    return res.json() as Promise<T>;
  }
}

const client = new MailerLiteClient({
  apiKey: process.env.MAILERLITE_API_KEY!,
});

Verify Connection

async function verifyConnection(client: MailerLiteClient): Promise<void> {
  const account = await client.request<{
    data: { email: string; name: string };
  }>("GET", "/account");
  console.log(`Connected as: ${account.data.name} (${account.data.email})`);
}

Core Patterns

Subscriber Management

interface Subscriber {
  id: string;
  email: string;
  status: "active" | "unsubscribed" | "unconfirmed" | "bounced" | "junk";
  fields: Record<string, string | null>;
  groups: Array<{ id: string; name: string }>;
  subscribed_at: string;
}

async function upsertSubscriber(
  client: MailerLiteClient,
  email: string,
  opts?: {
    fields?: Record<string, string>;
    groups?: string[];
    status?: "active" | "unsubscribed";
  }
): Promise<Subscriber> {
  const res = await client.request<{ data: Subscriber }>(
    "POST",
    "/subscribers",
    {
      email,
      fields: opts?.fields,
      groups: opts?.groups,
      status: opts?.status,
    }
  );
  return res.data;
}

async function listSubscribers(
  client: MailerLiteClient,
  params?: { limit?: number; cursor?: string; filter?: { status?: string } }
): Promise<{ data: Subscriber[]; meta: { next_cursor: string | null } }> {
  const query = new URLSearchParams();
  if (params?.limit) query.set("limit", String(params.limit));
  if (params?.cursor) query.set("cursor", params.cursor);
  if (params?.filter?.status) query.set("filter[status]", params.filter.status);

  return client.request("GET", `/subscribers?${query}`);
}

Group Management

interface Group {
  id: string;
  name: string;
  active_count: number;
  sent_count: number;
}

async function createGroup(
  client: MailerLiteClient,
  name: string
): Promise<Group> {
  const res = await client.request<{ data: Group }>("POST", "/groups", {
    name,
  });
  return res.data;
}

async function assignSubscriberToGroup(
  client: MailerLiteClient,
  subscriberId: string,
  groupId: string
): Promise<void> {
  await client.request(
    "POST",
    `/subscribers/${subscriberId}/groups/${groupId}`,
    {}
  );
}

Campaign Creation and Sending

interface Campaign {
  id: string;
  name: string;
  status: "draft" | "ready" | "sent" | "sending";
  type: "regular" | "ab" | "resend";
  emails: Array<{
    id: string;
    subject: string;
    content: string;
  }>;
}

async function createCampaign(
  client: MailerLiteClient,
  opts: {
    name: string;
    subject: string;
    content: string;
    groupIds: string[];
    from?: string;
    fromName?: string;
  }
): Promise<Campaign> {
  const res = await client.request<{ data: Campaign }>(
    "POST",
    "/campaigns",
    {
      name: opts.name,
      type: "regular",
      emails: [
        {
          subject: opts.subject,
          from: opts.from,
          from_name: opts.fromName,
          content: opts.content,
        },
      ],
      groups: opts.groupIds,
    }
  );
  return res.data;
}

async function scheduleCampaign(
  client: MailerLiteClient,
  campaignId: string,
  scheduleAt?: string // ISO 8601 datetime, omit for immediate
): Promise<void> {
  const body: Record<string, unknown> = { delivery: "instant" };
  if (scheduleAt) {
    body.delivery = "scheduled";
    body.schedule = { date: scheduleAt };
  }
  await client.request("POST", `/campaigns/${campaignId}/schedule`, body);
}

Campaign Analytics

async function getCampaignReport(
  client: MailerLiteClient,
  campaignId: string
): Promise<{
  sent: number;
  opens_count: number;
  clicks_count: number;
  open_rate: number;
  click_rate: number;
  unsubscribe_count: number;
}> {
  const res = await client.request<{
    data: {
      stats: {
        sent: number;
        opens_count: number;
        clicks_count: number;
        open_rate: { float: number };
        click_rate: { float: number };
        unsubscribe_count: number;
      };
    };
  }>("GET", `/campaigns/${campaignId}`);

  const s = res.data.stats;
  return {
    sent: s.sent,
    opens_count: s.opens_count,
    clicks_count: s.clicks_count,
    open_rate: s.open_rate.float,
    click_rate: s.click_rate.float,
    unsubscribe_count: s.unsubscribe_count,
  };
}

Cursor-Based Pagination Helper

async function* paginateAll<T>(
  client: MailerLiteClient,
  path: string,
  limit = 100
): AsyncGenerator<T> {
  let cursor: string | null = null;
  do {
    const sep = path.includes("?") ? "&" : "?";
    const cursorParam = cursor ? `&cursor=${cursor}` : "";
    const res = await client.request<{
      data: T[];
      meta: { next_cursor: string | null };
    }>("GET", `${path}${sep}limit=${limit}${cursorParam}`);

    for (const item of res.data) yield item;
    cursor = res.meta.next_cursor;
  } while (cursor);
}

Best Practices

  • Use upsert for subscriber creation: The POST /subscribers endpoint is idempotent. It creates or updates, so you never need to check existence first.
  • Leverage cursor-based pagination: MailerLite v2 uses cursor pagination, not page numbers. Always follow the next_cursor rather than guessing offsets.
  • Group subscribers at creation time: Pass group IDs in the subscriber creation call to avoid a second API request for group assignment.

Common Pitfalls

  • Exceeding rate limits during bulk imports: The 120 req/min limit is strict. Batch subscribers using the bulk endpoint or add delays between individual calls.
  • Sending HTML without testing: MailerLite strips certain HTML attributes for security. Always preview campaigns before scheduling to verify rendering.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add newsletter-marketing-services-skills

Get CLI access →