Beehiiv
"Beehiiv: newsletter platform API, subscriber management, publications, posts/campaigns, automations, referral program, analytics, REST API"
Beehiiv is a modern newsletter platform built for growth. Its API (v2) provides programmatic access to publications, subscribers, posts, automations, referral programs, and analytics. The API uses REST conventions with Bearer token authentication and JSON payloads. All requests go through `https://api.beehiiv.com/v2/`. Rate limits are generous but enforce per-second caps. Design integrations that batch subscriber operations, cache publication metadata, and handle pagination for list endpoints. ## Key Points - **Use publication-scoped endpoints**: Always include the publication ID in paths rather than relying on default publication resolution. - **Paginate subscriber lists**: Never assume all subscribers fit in one response. Use the pagination helper for exports or syncs. - **Cache publication metadata**: Publication details rarely change; cache them to reduce API calls. - **Set UTM parameters on subscriber creation**: Tag subscribers with utm_source and utm_medium at creation time for accurate attribution. - **Use `expand=stats` sparingly**: Post stats expansion adds latency; fetch stats only when needed for dashboards. - **Handle 409 for existing subscribers**: The API returns 409 when a subscriber already exists. Treat this as idempotent success, not an error. - **Prefer webhooks over polling**: Beehiiv supports webhooks for subscription events. Use them instead of polling the subscriber list. - **Ignoring rate limits**: Sending hundreds of concurrent requests without throttling will trigger 429 responses. Batch operations and add delays between bursts. - **Storing API keys in client-side code**: Beehiiv API keys have full publication access. Never expose them in frontend bundles or public repositories. - **Sending welcome emails during bulk imports**: When migrating subscribers, set `send_welcome_email: false` to avoid spamming imported users. - **Hardcoding publication IDs**: Use environment variables or configuration files. Publications can be recreated during testing. - **Fetching all posts to find one by title**: Use the search/filter parameters on the list endpoint rather than downloading every post to filter client-side.
skilldb get newsletter-marketing-services-skills/BeehiivFull skill: 307 linesBeehiiv Newsletter Platform Integration
Core Philosophy
Beehiiv is a modern newsletter platform built for growth. Its API (v2) provides programmatic access to publications, subscribers, posts, automations, referral programs, and analytics. The API uses REST conventions with Bearer token authentication and JSON payloads. All requests go through https://api.beehiiv.com/v2/. Rate limits are generous but enforce per-second caps. Design integrations that batch subscriber operations, cache publication metadata, and handle pagination for list endpoints.
Setup
Authentication and Client Configuration
interface BeehiivConfig {
apiKey: string;
publicationId: string;
baseUrl?: string;
}
class BeehiivClient {
private readonly baseUrl: string;
private readonly headers: Record<string, string>;
private readonly publicationId: string;
constructor(config: BeehiivConfig) {
this.baseUrl = config.baseUrl ?? "https://api.beehiiv.com/v2";
this.publicationId = config.publicationId;
this.headers = {
Authorization: `Bearer ${config.apiKey}`,
"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(
`Beehiiv API ${res.status}: ${JSON.stringify(error)}`
);
}
return res.json() as Promise<T>;
}
}
const client = new BeehiivClient({
apiKey: process.env.BEEHIIV_API_KEY!,
publicationId: process.env.BEEHIIV_PUBLICATION_ID!,
});
Verifying Connection
async function verifyConnection(client: BeehiivClient): Promise<void> {
const pub = await client.request<{ data: { id: string; name: string } }>(
"GET",
`/publications/${client["publicationId"]}`
);
console.log(`Connected to publication: ${pub.data.name}`);
}
Key Techniques
Subscriber Management
interface Subscriber {
id: string;
email: string;
status: "active" | "inactive" | "validating";
created_at: number;
utm_source?: string;
custom_fields?: Array<{ name: string; value: string }>;
}
async function createSubscriber(
client: BeehiivClient,
pubId: string,
email: string,
opts?: {
utmSource?: string;
utmMedium?: string;
referringSite?: string;
customFields?: Array<{ name: string; value: string }>;
sendWelcomeEmail?: boolean;
}
): Promise<Subscriber> {
const res = await client.request<{ data: Subscriber }>(
"POST",
`/publications/${pubId}/subscriptions`,
{
email,
reactivate_existing: false,
send_welcome_email: opts?.sendWelcomeEmail ?? true,
utm_source: opts?.utmSource,
utm_medium: opts?.utmMedium,
referring_site: opts?.referringSite,
custom_fields: opts?.customFields,
}
);
return res.data;
}
async function listSubscribers(
client: BeehiivClient,
pubId: string,
params?: { status?: string; limit?: number; page?: number }
): Promise<{ data: Subscriber[]; total_results: number }> {
const query = new URLSearchParams();
if (params?.status) query.set("status", params.status);
if (params?.limit) query.set("limit", String(params.limit));
if (params?.page) query.set("page", String(params.page));
return client.request("GET",
`/publications/${pubId}/subscriptions?${query}`
);
}
async function bulkSubscriberSync(
client: BeehiivClient,
pubId: string,
emails: string[],
source: string
): Promise<{ created: number; existing: number }> {
let created = 0;
let existing = 0;
// Beehiiv recommends batches of 1000
const batchSize = 1000;
for (let i = 0; i < emails.length; i += batchSize) {
const batch = emails.slice(i, i + batchSize);
const results = await Promise.allSettled(
batch.map((email) =>
createSubscriber(client, pubId, email, {
utmSource: source,
sendWelcomeEmail: false,
})
)
);
for (const r of results) {
if (r.status === "fulfilled") created++;
else existing++;
}
}
return { created, existing };
}
Posts and Campaigns
interface Post {
id: string;
title: string;
subtitle?: string;
status: "draft" | "confirmed" | "archived";
publish_date?: number;
web_url?: string;
audience: "free" | "premium" | "both";
}
async function createPost(
client: BeehiivClient,
pubId: string,
post: {
title: string;
subtitle?: string;
content: string; // HTML
audience?: "free" | "premium" | "both";
}
): Promise<Post> {
const res = await client.request<{ data: Post }>(
"POST",
`/publications/${pubId}/posts`,
{
title: post.title,
subtitle: post.subtitle,
content_html: post.content,
audience: post.audience ?? "both",
status: "draft",
}
);
return res.data;
}
async function getPostAnalytics(
client: BeehiivClient,
pubId: string,
postId: string
): Promise<{
sends: number;
opens: number;
clicks: number;
openRate: number;
}> {
const res = await client.request<{
data: {
stats: {
email: { recipients: number; opens: number; clicks: number };
};
};
}>("GET", `/publications/${pubId}/posts/${postId}?expand=stats`);
const s = res.data.stats.email;
return {
sends: s.recipients,
opens: s.opens,
clicks: s.clicks,
openRate: s.recipients > 0 ? s.opens / s.recipients : 0,
};
}
Referral Program
async function getReferralProgram(
client: BeehiivClient,
pubId: string
): Promise<{
enabled: boolean;
milestones: Array<{ referrals: number; reward: string }>;
}> {
const res = await client.request<{ data: any }>(
"GET",
`/publications/${pubId}/referral_program`
);
return res.data;
}
async function getSubscriberReferrals(
client: BeehiivClient,
pubId: string,
subscriberId: string
): Promise<{ referral_count: number; referral_url: string }> {
const res = await client.request<{ data: Subscriber & {
referral_count: number;
referral_url: string;
} }>(
"GET",
`/publications/${pubId}/subscriptions/${subscriberId}`
);
return {
referral_count: res.data.referral_count,
referral_url: res.data.referral_url,
};
}
Pagination Helper
async function* paginateAll<T>(
client: BeehiivClient,
path: string,
limit = 100
): AsyncGenerator<T> {
let page = 1;
while (true) {
const sep = path.includes("?") ? "&" : "?";
const res = await client.request<{
data: T[];
total_results: number;
}>("GET", `${path}${sep}limit=${limit}&page=${page}`);
for (const item of res.data) yield item;
if (res.data.length < limit) break;
page++;
}
}
Best Practices
- Use publication-scoped endpoints: Always include the publication ID in paths rather than relying on default publication resolution.
- Paginate subscriber lists: Never assume all subscribers fit in one response. Use the pagination helper for exports or syncs.
- Cache publication metadata: Publication details rarely change; cache them to reduce API calls.
- Set UTM parameters on subscriber creation: Tag subscribers with utm_source and utm_medium at creation time for accurate attribution.
- Use
expand=statssparingly: Post stats expansion adds latency; fetch stats only when needed for dashboards. - Handle 409 for existing subscribers: The API returns 409 when a subscriber already exists. Treat this as idempotent success, not an error.
- Prefer webhooks over polling: Beehiiv supports webhooks for subscription events. Use them instead of polling the subscriber list.
Anti-Patterns
- Ignoring rate limits: Sending hundreds of concurrent requests without throttling will trigger 429 responses. Batch operations and add delays between bursts.
- Storing API keys in client-side code: Beehiiv API keys have full publication access. Never expose them in frontend bundles or public repositories.
- Sending welcome emails during bulk imports: When migrating subscribers, set
send_welcome_email: falseto avoid spamming imported users. - Hardcoding publication IDs: Use environment variables or configuration files. Publications can be recreated during testing.
- Fetching all posts to find one by title: Use the search/filter parameters on the list endpoint rather than downloading every post to filter client-side.
- Skipping subscriber status checks: Always verify subscriber status is
activebefore targeting them in custom campaigns. Sending to inactive subscribers hurts deliverability.
Install this skill directly: skilldb add newsletter-marketing-services-skills
Related Skills
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"
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"