BYTETOOLS

Substack Public Posts API

Undocumented but public Substack endpoint listing a publication's posts as JSON, with titles, dates, reaction counts, word counts and paywall status. Tested example included.

No API key requiredHTTPSFree tier

Endpoint tested and returned HTTP 200 on 2026-08-21

What is the Substack Public Posts API?

Every Substack publication serves its post list as JSON at `/api/v1/posts` on its own domain, with no API key. Each post returns its title, subtitle, slug, publication date, audience setting, reaction and comment counts, word count and cover image.

Substack has never shipped a documented public API, but the endpoint its own web front-end calls is open on every publication and needs nothing to read. Point it at any Substack domain — the `*.substack.com` subdomain or a custom domain like the one in the example — and the post list comes back as JSON, ordered newest first.

The metadata is richer than the equivalent RSS feed by a wide margin. `audience` tells you whether a post is public or subscriber-only; `wordcount`, `reactions`, `restacks` and `comment_count` give you engagement figures RSS simply does not carry; `truncated_body_text` provides a preview even for paywalled posts. The obvious caveat is that undocumented means unstable — the field set can change without warning, and Substack's terms govern what you do with the content regardless of how easy it is to fetch. Custom-domain publications redirect off `*.substack.com`, so follow redirects or use the canonical domain directly.

Quick facts

Base URL
https://www.astralcodexten.com/api/v1
Authentication
No API key. Paywalled post bodies stay truncated without a subscriber session; metadata for them is still returned.
Rate limit
No published limit. It backs a live site — keep requests slow and cache results.
Pricing
Free to read. Content remains the publication's copyright.
CORS
Not enabled — call it from your server
Official docs
Read the docs

How to use the Substack Public Posts API

Every request below was executed against the live API on 2026-08-21, and the response shown is the real body it returned — not an illustration.

1. List a publication's most recent posts

GET https://www.astralcodexten.com/api/v1/posts?limit=1

curl
curl 'https://www.astralcodexten.com/api/v1/posts?limit=1'
JavaScript (fetch)
const res = await fetch("https://www.astralcodexten.com/api/v1/posts?limit=1");
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
const data = await res.json();
console.log(data);
Python (requests)
import requests

res = requests.get("https://www.astralcodexten.com/api/v1/posts?limit=1", timeout=20)
res.raise_for_status()
print(res.json())
Response — HTTP 200 (truncated)
[
  {
    "audience": "everyone",
    "audience_before_archived": null,
    "canonical_url": "https://www.astralcodexten.com/p/college-ea-meetups-everywhere-call",
    "default_comment_sort": null,
    "editor_v2": false,
    "exempt_from_archive_paywall": false,
    "free_unlock_required": false,
    "id": 212052999,
    "podcast_art_url": null,
    "podcast_duration": null,
    "podcast_preview_upload_id": null,
    "podcast_upload_id": null,
    "podcast_url": null,
    "post_date": "2026-08-20T19:55:39.806Z",
    "updated_at": "2026-08-20T19:58:27.477Z",
    "publication_id": 89120,
    "search_engine_description": null,
    "search_engine_title": null,
    "section_id": null,
    "should_send_free_preview": false,
    "show_guest_bios": true,
    "slug": "college-ea-meetups-everywhere-call",
    "social_title": "College EA Meetups Everywhere",
    "subtitle": "...",
    "teaser_post_eligible": true,
    "title": "College EA Meetups Everywhere: Call For Organizers",
    "type": "newsletter",
    "video_upload_id": null,
    "write_comment_permissions": "everyone",
    "meter_type": "none",
    "live_stream_id": null,
    "is_published": true,
    "restacks": 9,
    "reactions": {
      "❤": 76
    },
    "top_exclusions": [],
    "pins": [],
    "section_pins": [],
    "has_shareable_clips": false,
    "previous_post_slug": "why-im-staying-out-of-the-substack",
    "next_post_slug": null,
    "cover_image": "https://substack-post-media.s3.amazonaws.com/public/images/27645a54-8b36-4494-9413-62d22a675cd7_1999x1268.png",
    "cover_image_is_square": false,
    "cover_image

Parameters

ParameterTypeRequiredDescription
limitqueryOptionalHow many posts to return. Twelve by default; large values are throttled. 1
offsetqueryOptionalHow many posts to skip, for paging back through the archive. 12
sortqueryOptional`new` for reverse-chronological, `top` for most popular. new
(publication)base URLRequiredThe publication's own domain — either `name.substack.com` or its custom domain. https://www.astralcodexten.com

Response fields

(root)array
Posts, newest first. There is no envelope object.
title / subtitle / slugstring
Headline, standfirst and URL slug. Build the post URL from `canonical_url`, which is already absolute.
post_date / updated_atstring
ISO 8601 UTC timestamps for first publication and last edit.
audiencestring
`everyone` for public posts, `only_paid` for subscriber-only. This is how you detect the paywall.
typestring
`newsletter`, `podcast` or `thread` — determines which of the podcast and audio fields are populated.
wordcountinteger
Word count of the full post, present even when the body is truncated.
reactions / reaction_count / restacksobject / integer
Engagement figures. `reactions` is a map keyed by reaction type.
comment_count / child_comment_countinteger
Top-level comments and replies.
truncated_body_textstring
Plain-text preview. For paywalled posts this is all you get.
cover_image / coverImagePalettestring / object
Header image URL and its extracted colour palette.
publishedBylinesarray
Authors, with names, handles and profile images.

What you can build with the Substack Public Posts API

  • Mirror a newsletter's archive into your own site or search index
  • Track publishing cadence and engagement across several publications
  • Build a reading list spanning multiple Substacks in one place
  • Detect which posts are public before linking to them

Common errors and how to fix them

Redirect to a different host

The publication uses a custom domain.

Fix: Follow redirects, or call the canonical domain directly. `*.substack.com` 301s to the custom domain when one is configured.

Body text is cut off

The post is subscriber-only.

Fix: `audience` will read `only_paid`. Metadata and `truncated_body_text` are all that is available without a subscriber session.

Fields disappear after a Substack deploy

The endpoint is undocumented and covered by no compatibility promise.

Fix: Read defensively, treat every field as optional, and do not build anything load-bearing on a specific key.

Requests start failing

You are hitting infrastructure that serves a live site.

Fix: Slow down, cache, and page with `offset` rather than requesting huge limits.

Substack Public Posts API — frequently asked questions

Is there an official Substack API?

No. This endpoint powers Substack's own front-end and is publicly readable, but it is undocumented and unsupported, so treat its stability as a courtesy rather than a guarantee.

Can I read paywalled posts through it?

No. Posts marked `audience: only_paid` return metadata and a truncated preview only. The full body needs a subscriber session.

Which URL do I use for a publication?

Its own domain. `example.substack.com/api/v1/posts` works, and publications on a custom domain redirect there — the example uses a custom domain directly to skip the hop.

Is it legal to use this data?

Reading public metadata is straightforward, but the content is copyright the publication and Substack's terms of service apply. Republishing post bodies is a very different proposition from linking to them.

Tools that pair with this API

Substack Public Posts is an independent third-party service and is not affiliated with ByteTools or ByteVancer. Details on this page were verified on 2026-08-21; always check the official documentation before relying on this API in production, as terms and limits can change.