Skip to main content
Business & GrowthNewsletter Marketing Services241 lines

Substack

"Substack: newsletter publishing platform, post management, subscriber import/export, custom domains, paid subscriptions, RSS feed integration"

Quick Summary11 lines
You are an expert in integrating Substack for newsletter publishing and subscriber management.

## Key Points

- **Use RSS for content syndication**: Substack's RSS feed is the most reliable integration point. Poll it for new content rather than scraping the website.
- **Export subscribers regularly**: Since there is no subscriber API, schedule regular CSV exports from the Substack dashboard for backup and cross-platform sync.
- **Set up a custom domain early**: Moving to a custom domain later requires DNS changes and can temporarily break subscriber links. Configure it before growing your audience.
- **Assuming a REST API exists**: Substack does not offer a public API for subscriber management or post creation. Do not build integrations expecting programmatic write access.
- **Scraping instead of using RSS**: Web scraping Substack pages is fragile and violates terms of service. Always prefer the RSS feed for reading published content.
skilldb get newsletter-marketing-services-skills/SubstackFull skill: 241 lines
Paste into your CLAUDE.md or agent config

Substack — Newsletter & Email Marketing

You are an expert in integrating Substack for newsletter publishing and subscriber management.

Core Philosophy

Substack is a newsletter-first publishing platform focused on writers and independent publishers. Unlike API-heavy platforms, Substack does not expose a public REST API for third-party integrations. Instead, integrations rely on RSS feeds for content syndication, CSV-based subscriber import/export, embed widgets for signup forms, and the Substack custom domain setup for branding. For programmatic workflows, use the RSS feed, oEmbed endpoints, and webhook-like patterns via Zapier or custom RSS polling. Design integrations around content consumption (RSS), subscriber acquisition (embeds and landing pages), and data portability (CSV export).

Setup & Configuration

RSS Feed Access

Every Substack publication exposes an RSS feed at https://<publication>.substack.com/feed. For custom domains, the feed is at https://<yourdomain.com>/feed.

import Parser from "rss-parser";

interface SubstackConfig {
  publicationUrl: string; // e.g., "https://example.substack.com" or custom domain
}

class SubstackReader {
  private readonly feedUrl: string;
  private readonly parser: Parser;

  constructor(config: SubstackConfig) {
    this.feedUrl = `${config.publicationUrl.replace(/\/$/, "")}/feed`;
    this.parser = new Parser({
      customFields: {
        item: ["enclosure", "dc:creator"],
      },
    });
  }

  async getLatestPosts(limit = 10): Promise<SubstackPost[]> {
    const feed = await this.parser.parseURL(this.feedUrl);
    return feed.items.slice(0, limit).map((item) => ({
      title: item.title ?? "",
      link: item.link ?? "",
      pubDate: item.pubDate ? new Date(item.pubDate) : new Date(),
      author: (item as any)["dc:creator"] ?? "",
      content: item["content:encoded"] ?? item.content ?? "",
      summary: item.contentSnippet ?? "",
      guid: item.guid ?? item.link ?? "",
    }));
  }
}

interface SubstackPost {
  title: string;
  link: string;
  pubDate: Date;
  author: string;
  content: string;
  summary: string;
  guid: string;
}

const reader = new SubstackReader({
  publicationUrl: process.env.SUBSTACK_URL ?? "https://example.substack.com",
});

Custom Domain Configuration

# DNS records required for custom domain on Substack
# 1. CNAME record: point your subdomain to target.substack-custom-domains.com
# 2. Or for apex domain, use A records pointing to Substack's IPs

# Example DNS setup (via your DNS provider):
# Type: CNAME
# Host: newsletter       (for newsletter.yourdomain.com)
# Value: target.substack-custom-domains.com
# TTL: 3600

# Then configure in Substack dashboard:
# Settings > Publication details > Custom domain

Core Patterns

RSS Feed Polling for New Post Detection

class SubstackPoller {
  private readonly reader: SubstackReader;
  private seenGuids: Set<string> = new Set();

  constructor(reader: SubstackReader) {
    this.reader = reader;
  }

  async checkForNewPosts(): Promise<SubstackPost[]> {
    const posts = await this.reader.getLatestPosts(20);
    const newPosts: SubstackPost[] = [];

    for (const post of posts) {
      if (!this.seenGuids.has(post.guid)) {
        this.seenGuids.add(post.guid);
        newPosts.push(post);
      }
    }

    return newPosts;
  }

  async startPolling(
    intervalMs: number,
    onNewPost: (post: SubstackPost) => Promise<void>
  ): Promise<void> {
    // Seed with existing posts
    const initial = await this.reader.getLatestPosts(20);
    for (const p of initial) this.seenGuids.add(p.guid);

    setInterval(async () => {
      const newPosts = await this.checkForNewPosts();
      for (const post of newPosts) {
        await onNewPost(post);
      }
    }, intervalMs);
  }
}

Subscriber CSV Import/Export Handling

import { parse, stringify } from "csv-parse/sync";
import fs from "fs";

interface SubstackSubscriber {
  email: string;
  active: boolean;
  type: "free" | "paid";
  created_at: string;
}

function parseSubstackExport(csvPath: string): SubstackSubscriber[] {
  const content = fs.readFileSync(csvPath, "utf-8");
  const records = parse(content, {
    columns: true,
    skip_empty_lines: true,
  });

  return records.map((r: any) => ({
    email: r.email,
    active: r.active_subscription === "true",
    type: r.type ?? "free",
    created_at: r.created_at,
  }));
}

function generateImportCsv(
  emails: string[],
  outputPath: string
): void {
  // Substack import expects a CSV with an "email" column
  const header = "email\n";
  const rows = emails.join("\n");
  fs.writeFileSync(outputPath, header + rows, "utf-8");
  console.log(`Import CSV written: ${outputPath} (${emails.length} emails)`);
}

oEmbed for Embedding Substack Posts

async function getSubstackEmbed(
  postUrl: string
): Promise<{ html: string; width: number; height: number }> {
  const oembedUrl = `https://substack.com/oembed?url=${encodeURIComponent(
    postUrl
  )}`;
  const res = await fetch(oembedUrl);
  if (!res.ok) throw new Error(`oEmbed failed: ${res.status}`);
  return res.json() as Promise<{
    html: string;
    width: number;
    height: number;
  }>;
}

Cross-Posting from Substack to Other Platforms

async function crossPostLatest(
  reader: SubstackReader,
  publisher: (post: SubstackPost) => Promise<void>
): Promise<void> {
  const posts = await reader.getLatestPosts(1);
  if (posts.length === 0) return;

  const latest = posts[0];
  await publisher(latest);
  console.log(`Cross-posted: ${latest.title}`);
}

// Example: cross-post to a CMS or social platform
await crossPostLatest(reader, async (post) => {
  await fetch("https://your-cms.com/api/posts", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      title: post.title,
      body: post.content,
      canonical_url: post.link,
      published_at: post.pubDate.toISOString(),
    }),
  });
});

Best Practices

  • Use RSS for content syndication: Substack's RSS feed is the most reliable integration point. Poll it for new content rather than scraping the website.
  • Export subscribers regularly: Since there is no subscriber API, schedule regular CSV exports from the Substack dashboard for backup and cross-platform sync.
  • Set up a custom domain early: Moving to a custom domain later requires DNS changes and can temporarily break subscriber links. Configure it before growing your audience.

Common Pitfalls

  • Assuming a REST API exists: Substack does not offer a public API for subscriber management or post creation. Do not build integrations expecting programmatic write access.
  • Scraping instead of using RSS: Web scraping Substack pages is fragile and violates terms of service. Always prefer the RSS feed for reading published content.

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

Get CLI access →