Buttondown
"Buttondown: minimal newsletter API, subscribers, emails/drafts, tags, automations, webhooks, Markdown-first, REST API"
Buttondown is a minimalist, Markdown-first newsletter platform with a clean REST API at `https://api.buttondown.email/v1/`. It prioritizes simplicity and developer experience: flat resource endpoints, predictable pagination, and Markdown as the primary content format. Authentication uses a single API key via header. Buttondown is ideal for developers and writers who want programmatic newsletter management without the complexity of enterprise email platforms. Design integrations that embrace Markdown content, use tags for lightweight segmentation, and leverage webhooks for event-driven workflows. ## Key Points - **Write content in Markdown**: Buttondown's core strength is Markdown rendering. Send Markdown in the `body` field and let the platform handle HTML conversion. - **Use `draft` status for review workflows**: Create emails as drafts, review them in the Buttondown UI, then update status to send or schedule programmatically. - **Leverage subscriber metadata**: The `metadata` field accepts arbitrary JSON. Use it for tracking custom attributes without creating separate database tables. - **Paginate all list endpoints**: Buttondown returns paginated results with `next`/`previous` URLs. Always follow pagination for complete data retrieval. - **Tag at creation time**: Assign tags when creating subscribers rather than making a separate update call. This reduces API calls and ensures immediate segmentation. - **Use webhooks for automation triggers**: Subscribe to `subscriber.created` and `email.sent` events to trigger downstream workflows in real time. - **Store subscriber IDs**: Buttondown uses UUIDs for subscribers. Cache these to avoid repeated lookups by email address. - **Sending HTML in the body field**: Buttondown expects Markdown. Sending raw HTML will result in double-rendered or broken content. Convert HTML to Markdown before submission. - **Polling for new subscribers**: Use the `subscriber.created` webhook instead of repeatedly fetching the subscriber list to detect new sign-ups. - **Ignoring subscriber_type**: Subscribers can be `unactivated` (double opt-in pending) or `removed`. Always filter by `regular` or `premium` when building active subscriber counts. - **Creating tags with duplicate names**: Buttondown allows duplicate tag names, which creates confusion. Always check existing tags before creating new ones. - **Setting status to `about_to_send` without review**: Once set, the email sends immediately. Use `draft` or `scheduled` status for anything that needs review.
skilldb get newsletter-marketing-services-skills/ButtondownFull skill: 354 linesButtondown Newsletter Platform Integration
Core Philosophy
Buttondown is a minimalist, Markdown-first newsletter platform with a clean REST API at https://api.buttondown.email/v1/. It prioritizes simplicity and developer experience: flat resource endpoints, predictable pagination, and Markdown as the primary content format. Authentication uses a single API key via header. Buttondown is ideal for developers and writers who want programmatic newsletter management without the complexity of enterprise email platforms. Design integrations that embrace Markdown content, use tags for lightweight segmentation, and leverage webhooks for event-driven workflows.
Setup
Authentication and Client Configuration
interface ButtondownConfig {
apiKey: string;
baseUrl?: string;
}
class ButtondownClient {
private readonly baseUrl: string;
private readonly headers: Record<string, string>;
constructor(config: ButtondownConfig) {
this.baseUrl = config.baseUrl ?? "https://api.buttondown.email/v1";
this.headers = {
Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json",
};
}
async request<T>(
method: string,
path: string,
body?: unknown,
params?: Record<string, string>
): Promise<T> {
const query = params ? `?${new URLSearchParams(params)}` : "";
const url = `${this.baseUrl}${path}${query}`;
const res = await fetch(url, {
method,
headers: this.headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const errorBody = await res.text();
throw new Error(`Buttondown ${method} ${path} ${res.status}: ${errorBody}`);
}
if (res.status === 204) return undefined as T;
return res.json() as Promise<T>;
}
}
const bd = new ButtondownClient({
apiKey: process.env.BUTTONDOWN_API_KEY!,
});
Verify Connection
async function verifyConnection(client: ButtondownClient): Promise<void> {
const res = await client.request<{ username: string; email: string }>(
"GET",
"/newsletters"
);
// v1 returns newsletter metadata; confirms API key is valid
console.log("Buttondown API connection verified");
}
Key Techniques
Subscriber Management
interface BDSubscriber {
id: string;
email: string;
notes: string;
creation_date: string;
tags: string[];
metadata: Record<string, unknown>;
subscriber_type: "regular" | "premium" | "gift" | "unactivated" | "removed";
source: string;
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
referrer_url?: string;
}
interface PaginatedResponse<T> {
count: number;
next: string | null;
previous: string | null;
results: T[];
}
async function createSubscriber(
client: ButtondownClient,
email: string,
opts?: {
notes?: string;
tags?: string[];
metadata?: Record<string, unknown>;
referrerUrl?: string;
utmSource?: string;
}
): Promise<BDSubscriber> {
return client.request<BDSubscriber>("POST", "/subscribers", {
email,
notes: opts?.notes ?? "",
tags: opts?.tags ?? [],
metadata: opts?.metadata ?? {},
referrer_url: opts?.referrerUrl,
utm_source: opts?.utmSource,
});
}
async function listSubscribers(
client: ButtondownClient,
opts?: { page?: number; type?: string; tag?: string }
): Promise<PaginatedResponse<BDSubscriber>> {
const params: Record<string, string> = {};
if (opts?.page) params.page = String(opts.page);
if (opts?.type) params.subscriber_type = opts.type;
if (opts?.tag) params.tag = opts.tag;
return client.request("GET", "/subscribers", undefined, params);
}
async function updateSubscriber(
client: ButtondownClient,
subscriberId: string,
updates: Partial<{
email: string;
notes: string;
tags: string[];
metadata: Record<string, unknown>;
subscriber_type: string;
}>
): Promise<BDSubscriber> {
return client.request<BDSubscriber>(
"PATCH",
`/subscribers/${subscriberId}`,
updates
);
}
async function deleteSubscriber(
client: ButtondownClient,
subscriberId: string
): Promise<void> {
await client.request("DELETE", `/subscribers/${subscriberId}`);
}
async function* getAllSubscribers(
client: ButtondownClient
): AsyncGenerator<BDSubscriber> {
let page = 1;
while (true) {
const res = await listSubscribers(client, { page });
for (const sub of res.results) yield sub;
if (!res.next) break;
page++;
}
}
Emails and Drafts
interface BDEmail {
id: string;
subject: string;
body: string; // Markdown
creation_date: string;
modification_date: string;
publish_date: string | null;
status: "draft" | "scheduled" | "sent" | "imported";
email_type: "public" | "private" | "premium";
secondary_id: number;
slug: string;
external_url: string;
}
async function createDraft(
client: ButtondownClient,
subject: string,
body: string, // Markdown content
opts?: { emailType?: "public" | "private" | "premium"; slug?: string }
): Promise<BDEmail> {
return client.request<BDEmail>("POST", "/emails", {
subject,
body,
status: "draft",
email_type: opts?.emailType ?? "public",
slug: opts?.slug,
});
}
async function sendEmail(
client: ButtondownClient,
subject: string,
markdownBody: string,
opts?: { emailType?: "public" | "private" | "premium" }
): Promise<BDEmail> {
return client.request<BDEmail>("POST", "/emails", {
subject,
body: markdownBody,
status: "about_to_send",
email_type: opts?.emailType ?? "public",
});
}
async function scheduleEmail(
client: ButtondownClient,
emailId: string,
publishDate: Date
): Promise<BDEmail> {
return client.request<BDEmail>("PATCH", `/emails/${emailId}`, {
status: "scheduled",
publish_date: publishDate.toISOString(),
});
}
async function listEmails(
client: ButtondownClient,
status?: string
): Promise<PaginatedResponse<BDEmail>> {
const params: Record<string, string> = {};
if (status) params.status = status;
return client.request("GET", "/emails", undefined, params);
}
Tags
interface BDTag {
id: string;
name: string;
creation_date: string;
description: string;
color: string;
}
async function createTag(
client: ButtondownClient,
name: string,
opts?: { description?: string; color?: string }
): Promise<BDTag> {
return client.request<BDTag>("POST", "/tags", {
name,
description: opts?.description ?? "",
color: opts?.color ?? "#000000",
});
}
async function listTags(client: ButtondownClient): Promise<BDTag[]> {
const res = await client.request<PaginatedResponse<BDTag>>("GET", "/tags");
return res.results;
}
async function tagSubscribers(
client: ButtondownClient,
tagName: string,
emails: string[]
): Promise<void> {
// Find or create the tag
const tags = await listTags(client);
let tag = tags.find((t) => t.name === tagName);
if (!tag) {
tag = await createTag(client, tagName);
}
// Update each subscriber to include the tag
for (const email of emails) {
const subs = await listSubscribers(client, { page: 1 });
const sub = subs.results.find((s) => s.email === email);
if (sub && !sub.tags.includes(tag.id)) {
await updateSubscriber(client, sub.id, {
tags: [...sub.tags, tag.id],
});
}
}
}
Webhooks
interface BDWebhook {
id: string;
url: string;
event_type: string;
creation_date: string;
}
type BDWebhookEvent =
| "subscriber.created"
| "subscriber.updated"
| "subscriber.deleted"
| "subscriber.confirmed"
| "subscriber.unsubscribed"
| "email.created"
| "email.sent"
| "email.drafted";
async function createWebhook(
client: ButtondownClient,
url: string,
eventType: BDWebhookEvent
): Promise<BDWebhook> {
return client.request<BDWebhook>("POST", "/webhooks", {
url,
event_type: eventType,
});
}
async function listWebhooks(
client: ButtondownClient
): Promise<BDWebhook[]> {
const res = await client.request<PaginatedResponse<BDWebhook>>(
"GET",
"/webhooks"
);
return res.results;
}
Best Practices
- Write content in Markdown: Buttondown's core strength is Markdown rendering. Send Markdown in the
bodyfield and let the platform handle HTML conversion. - Use
draftstatus for review workflows: Create emails as drafts, review them in the Buttondown UI, then update status to send or schedule programmatically. - Leverage subscriber metadata: The
metadatafield accepts arbitrary JSON. Use it for tracking custom attributes without creating separate database tables. - Paginate all list endpoints: Buttondown returns paginated results with
next/previousURLs. Always follow pagination for complete data retrieval. - Tag at creation time: Assign tags when creating subscribers rather than making a separate update call. This reduces API calls and ensures immediate segmentation.
- Use webhooks for automation triggers: Subscribe to
subscriber.createdandemail.sentevents to trigger downstream workflows in real time. - Store subscriber IDs: Buttondown uses UUIDs for subscribers. Cache these to avoid repeated lookups by email address.
Anti-Patterns
- Sending HTML in the body field: Buttondown expects Markdown. Sending raw HTML will result in double-rendered or broken content. Convert HTML to Markdown before submission.
- Polling for new subscribers: Use the
subscriber.createdwebhook instead of repeatedly fetching the subscriber list to detect new sign-ups. - Ignoring subscriber_type: Subscribers can be
unactivated(double opt-in pending) orremoved. Always filter byregularorpremiumwhen building active subscriber counts. - Creating tags with duplicate names: Buttondown allows duplicate tag names, which creates confusion. Always check existing tags before creating new ones.
- Setting status to
about_to_sendwithout review: Once set, the email sends immediately. Usedraftorscheduledstatus for anything that needs review. - Neglecting error response bodies: Buttondown returns detailed validation errors in the response body. Always parse and log these for debugging.
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"
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"
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"