From website-to-api
Retrieve posts from any Substack newsletter using its internal API. Use when asked to list, fetch, search, or download Substack articles/posts. Supports pagination, search, sorting, and full content retrieval for paid posts.
npx claudepluginhub hamelsmu/website-to-apiThis skill uses the workspace's default tool permissions.
Retrieve posts from any Substack newsletter via its internal API, with full authenticated access to paid/subscriber-only content.
Generates Substack Note ideas by scanning YouTube videos, newsletters, and prior Notes. Orchestrates fetching, processed-log management, duplicate prevention, and delegation to idea extraction. Use for content repurposing and posting cadence.
Fetches publicly visible Substack subscriber counts and post view counts via WebFetch to supplement stale CSV data (>24h old) or verify external shares. Rate-limited to ≤10 fetches.
Writes, optimizes, and grows Substack newsletters and web posts including ghostwriting with voice matching, algorithm optimization, Notes strategy, SEO, growth tactics, and monetization planning.
Share bugs, ideas, or general feedback.
Retrieve posts from any Substack newsletter via its internal API, with full authenticated access to paid/subscriber-only content.
Built using the website-to-api pattern. If this skill breaks, use the website-to-api meta-skill to re-discover the API.
This skill uses Substack's internal, undocumented API. If commands fail:
website-to-api meta-skill.Known past changes: the session cookie was renamed from connect.sid to substack.sid. The COOKIE_NAME constant in the script controls this.
SUBSTACK_SID environment variable set to your substack.sid cookie valueuv runsubstack.com pagehttps://substack.comsubstack.sid (httpOnly, Secure) and copy its valueexport SUBSTACK_SID="<value>"The cookie lasts ~3 months and survives MFA unless you sign out.
// Run via javascript_tool on a newsletter tab
fetch('/api/v1/archive?sort=new&limit=1', { credentials: 'include' })
.then(r => r.json())
.then(data => { document.title = JSON.stringify({ ok: true, title: data[0]?.title }); })
.catch(e => { document.title = 'ERROR: ' + e.message; });
skills/substack/scripts/substack.py (relative to plugin root)
uv run skills/substack/scripts/substack.py list-posts https://www.lennysnewsletter.com
uv run skills/substack/scripts/substack.py list-posts https://www.lennysnewsletter.com --limit 30 --output json
uv run skills/substack/scripts/substack.py list-posts https://www.lennysnewsletter.com --sort top --limit 10
uv run skills/substack/scripts/substack.py list-posts https://www.lennysnewsletter.com --search "AI"
uv run skills/substack/scripts/substack.py list-posts lenny # bare subdomain
| Option | Default | Description |
|---|---|---|
--limit | 12 | Number of posts (auto-paginates >50) |
--offset | 0 | Pagination offset |
--sort | new | new or top |
--search | (none) | Full-text search query |
--output | table | table or json |
--sid | (none) | Cookie value (overrides env var) |
uv run skills/substack/scripts/substack.py get-post https://www.lennysnewsletter.com my-post-slug
uv run skills/substack/scripts/substack.py get-post https://www.lennysnewsletter.com my-post-slug --output html
uv run skills/substack/scripts/substack.py get-post https://www.lennysnewsletter.com my-post-slug --output json
| Option | Default | Description |
|---|---|---|
--output | summary | summary, html, or json |
Fetch a post and save as Markdown with metadata header.
uv run skills/substack/scripts/substack.py get-text https://www.lennysnewsletter.com my-post-slug
uv run skills/substack/scripts/substack.py get-text https://www.lennysnewsletter.com my-post-slug --out ./article.md
| Option | Default | Description |
|---|---|---|
--out / -o | /tmp/.md | Output file path |
Newsletter argument accepts full URLs or bare subdomains (e.g. lenny → https://lenny.substack.com).
| Endpoint | Method | Description |
|---|---|---|
/api/v1/archive | GET | List/search posts |
/api/v1/posts/<slug> | GET | Get a single post |
| Param | Values |
|---|---|
sort | new, top |
search | any string |
offset | integer |
limit | integer (max 50) |
For audience: "only_paid" posts:
body_html truncated at paywall