Mailchimp
"Mailchimp: email marketing platform API, audience lists, campaigns, automations, segments, templates, analytics, REST API v3"
You are an expert in integrating Mailchimp for email marketing and newsletters.
## Key Points
- **Use PUT for subscriber upserts**: The `PUT /lists/{id}/members/{hash}` endpoint creates or updates in one call. Avoid separate GET-then-POST flows.
- **Prefer batch endpoints for bulk operations**: The `/lists/{id}` POST endpoint handles up to 500 members at once, far more efficient than individual calls.
- **Cache the subscriber hash**: Computing MD5 hashes is cheap, but consistent hashing (always lowercase the email first) prevents duplicate member records.
- **Forgetting the data center prefix**: The API key format is `<key>-<dc>`. Omitting the `dc` portion when constructing the base URL causes all requests to fail silently.skilldb get newsletter-marketing-services-skills/MailchimpFull skill: 308 linesMailchimp — Newsletter & Email Marketing
You are an expert in integrating Mailchimp for email marketing and newsletters.
Core Philosophy
Mailchimp is the most widely adopted email marketing platform with a mature REST API (v3) at https://<dc>.api.mailchimp.com/3.0/, where <dc> is the data center prefix from your API key (e.g., us21). It uses HTTP Basic Auth with any username and the API key as the password. Subscribers belong to audiences (formerly lists), and segmentation happens through tags, segments, and groups. The API enforces a 10 concurrent connection limit. Design integrations that respect batch operation endpoints, use webhooks for real-time sync, and handle the member hash (MD5 of lowercase email) for subscriber lookups.
Setup & Configuration
Authentication and Client Setup
import crypto from "crypto";
interface MailchimpConfig {
apiKey: string; // format: "<key>-<dc>"
baseUrl?: string;
}
class MailchimpClient {
private readonly baseUrl: string;
private readonly headers: Record<string, string>;
constructor(config: MailchimpConfig) {
const dc = config.apiKey.split("-").pop();
this.baseUrl =
config.baseUrl ?? `https://${dc}.api.mailchimp.com/3.0`;
this.headers = {
Authorization: `Basic ${Buffer.from(
`anystring:${config.apiKey}`
).toString("base64")}`,
"Content-Type": "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(
`Mailchimp API ${res.status}: ${JSON.stringify(error)}`
);
}
return res.json() as Promise<T>;
}
/** MD5 hash of lowercase email, required for member endpoints */
static subscriberHash(email: string): string {
return crypto
.createHash("md5")
.update(email.toLowerCase())
.digest("hex");
}
}
const client = new MailchimpClient({
apiKey: process.env.MAILCHIMP_API_KEY!,
});
Verify Connection
async function verifyConnection(client: MailchimpClient): Promise<void> {
const root = await client.request<{
account_name: string;
email: string;
}>("GET", "/");
console.log(
`Connected to Mailchimp: ${root.account_name} (${root.email})`
);
}
Core Patterns
Subscriber (Member) Management
interface Member {
id: string;
email_address: string;
status: "subscribed" | "unsubscribed" | "cleaned" | "pending" | "transactional";
merge_fields: Record<string, string>;
tags: Array<{ id: number; name: string }>;
list_id: string;
}
async function upsertMember(
client: MailchimpClient,
listId: string,
email: string,
opts?: {
status?: Member["status"];
mergeFields?: Record<string, string>;
tags?: string[];
}
): Promise<Member> {
const hash = MailchimpClient.subscriberHash(email);
const res = await client.request<Member>(
"PUT",
`/lists/${listId}/members/${hash}`,
{
email_address: email,
status_if_new: opts?.status ?? "subscribed",
merge_fields: opts?.mergeFields,
}
);
// Tags must be managed separately
if (opts?.tags?.length) {
await client.request(
"POST",
`/lists/${listId}/members/${hash}/tags`,
{
tags: opts.tags.map((name) => ({ name, status: "active" })),
}
);
}
return res;
}
async function getMember(
client: MailchimpClient,
listId: string,
email: string
): Promise<Member> {
const hash = MailchimpClient.subscriberHash(email);
return client.request<Member>(
"GET",
`/lists/${listId}/members/${hash}`
);
}
async function archiveMember(
client: MailchimpClient,
listId: string,
email: string
): Promise<void> {
const hash = MailchimpClient.subscriberHash(email);
await client.request("DELETE", `/lists/${listId}/members/${hash}`);
}
Campaign Creation and Sending
interface Campaign {
id: string;
type: "regular" | "plaintext" | "absplit" | "rss";
status: "save" | "paused" | "schedule" | "sending" | "sent";
settings: {
subject_line: string;
from_name: string;
reply_to: string;
};
}
async function createAndSendCampaign(
client: MailchimpClient,
opts: {
listId: string;
subject: string;
fromName: string;
replyTo: string;
htmlContent: string;
segmentId?: number;
}
): Promise<Campaign> {
// 1. Create campaign
const campaign = await client.request<Campaign>("POST", "/campaigns", {
type: "regular",
recipients: {
list_id: opts.listId,
segment_opts: opts.segmentId
? { saved_segment_id: opts.segmentId }
: undefined,
},
settings: {
subject_line: opts.subject,
from_name: opts.fromName,
reply_to: opts.replyTo,
},
});
// 2. Set content
await client.request("PUT", `/campaigns/${campaign.id}/content`, {
html: opts.htmlContent,
});
// 3. Send
await client.request("POST", `/campaigns/${campaign.id}/actions/send`);
return campaign;
}
Batch Operations for Bulk Imports
async function bulkUpsertMembers(
client: MailchimpClient,
listId: string,
members: Array<{
email: string;
mergeFields?: Record<string, string>;
tags?: string[];
}>
): Promise<{ created: number; updated: number; errors: number }> {
const res = await client.request<{
new_members: unknown[];
updated_members: unknown[];
errors: unknown[];
}>("POST", `/lists/${listId}`, {
members: members.map((m) => ({
email_address: m.email,
status_if_new: "subscribed",
merge_fields: m.mergeFields ?? {},
tags: m.tags ?? [],
})),
update_existing: true,
});
return {
created: res.new_members.length,
updated: res.updated_members.length,
errors: res.errors.length,
};
}
Campaign Reports
async function getCampaignReport(
client: MailchimpClient,
campaignId: string
): Promise<{
emails_sent: number;
opens: number;
unique_opens: number;
open_rate: number;
clicks: number;
click_rate: number;
unsubscribed: number;
bounce_rate: number;
}> {
const res = await client.request<{
emails_sent: number;
opens: { opens_total: number; unique_opens: number; open_rate: number };
clicks: { clicks_total: number; click_rate: number };
unsubscribed: number;
bounces: { hard_bounces: number; soft_bounces: number };
}>("GET", `/reports/${campaignId}`);
const totalBounces = res.bounces.hard_bounces + res.bounces.soft_bounces;
return {
emails_sent: res.emails_sent,
opens: res.opens.opens_total,
unique_opens: res.opens.unique_opens,
open_rate: res.opens.open_rate,
clicks: res.clicks.clicks_total,
click_rate: res.clicks.click_rate,
unsubscribed: res.unsubscribed,
bounce_rate: res.emails_sent > 0 ? totalBounces / res.emails_sent : 0,
};
}
Best Practices
- Use PUT for subscriber upserts: The
PUT /lists/{id}/members/{hash}endpoint creates or updates in one call. Avoid separate GET-then-POST flows. - Prefer batch endpoints for bulk operations: The
/lists/{id}POST endpoint handles up to 500 members at once, far more efficient than individual calls. - Cache the subscriber hash: Computing MD5 hashes is cheap, but consistent hashing (always lowercase the email first) prevents duplicate member records.
Common Pitfalls
- Forgetting the data center prefix: The API key format is
<key>-<dc>. Omitting thedcportion when constructing the base URL causes all requests to fail silently. - Managing tags in the member upsert call: Tags cannot be set via the PUT member endpoint. They require a separate POST to the
/tagssub-resource. Attempting to include tags in the upsert body silently ignores them.
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"
MailerLite
"MailerLite: email marketing platform API, subscriber groups, campaigns, automations, forms, analytics, REST API v2"
Resend
"Resend: developer-first email API, transactional and marketing emails, React Email templates, domains, audiences, broadcasts, REST API"