Skip to main content
Business & GrowthNewsletter Marketing Services307 lines

Beehiiv

"Beehiiv: newsletter platform API, subscriber management, publications, posts/campaigns, automations, referral program, analytics, REST API"

Quick Summary18 lines
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 lines
Paste into your CLAUDE.md or agent config

Beehiiv 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=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.

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: 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.
  • Skipping subscriber status checks: Always verify subscriber status is active before targeting them in custom campaigns. Sending to inactive subscribers hurts deliverability.

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

Get CLI access →