MailerLite
"MailerLite: email marketing platform API, subscriber groups, campaigns, automations, forms, analytics, REST API v2"
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 linesMailerLite — 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
/subscribersendpoint 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_cursorrather 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
Related Skills
Beehiiv
"Beehiiv: newsletter platform API, subscriber management, publications, posts/campaigns, automations, referral program, analytics, REST API"
Buttondown
"Buttondown: minimal newsletter API, subscribers, emails/drafts, tags, automations, webhooks, Markdown-first, REST API"
ConvertKit (Kit)
"ConvertKit (Kit): creator email platform, subscriber management, forms, sequences, broadcasts, tags, automations, commerce, REST API"
Loops
"Loops: email for SaaS, transactional + marketing, contacts, events, loops (automations), API sends, webhooks"
Mailchimp
"Mailchimp: email marketing platform API, audience lists, campaigns, automations, segments, templates, analytics, REST API v3"
Resend
"Resend: developer-first email API, transactional and marketing emails, React Email templates, domains, audiences, broadcasts, REST API"