API Reference

Complete reference for the Stackhooks REST API.

Getting Started

Authentication

All API requests require an API key passed in the Authorization header. You can create API keys in your dashboard.

bash
curl https://stackhooks.com/v1/publications/platformer \
  -H "Authorization: Bearer sk_live_your_api_key"

Base URL

All endpoints are prefixed with the following base URL. Responses are JSON with a { data: ... } envelope.

https://stackhooks.com/v1/

Endpoints

Get Publication

Retrieve metadata for a Substack publication including name, description, logo, and author list.

GET/v1/publications/:slug

Parameters

NameInTypeRequiredDescription
slugpathstringYesPublication slug (e.g. "platformer") or custom domain hostname (e.g. "www.platformer.news")

Example Request

bash
curl https://stackhooks.com/v1/publications/platformer \
  -H "Authorization: Bearer sk_live_abc123..."

Example Response

json
{
  "data": {
    "slug": "platformer",
    "name": "Platformer",
    "description": "Casey Newton covers the intersection of Silicon Valley and democracy.",
    "base_url": "https://www.platformer.news",
    "logo_url": "https://substackcdn.com/image/fetch/f_auto,q_auto:good/...",
    "hero_image_url": "https://substackcdn.com/image/fetch/...",
    "twitter_handle": "platformer",
    "author_count": 1,
    "authors": [
      {
        "id": 2013012,
        "name": "Casey Newton",
        "handle": "caseynewton",
        "photo_url": "https://substackcdn.com/image/...",
        "bio": "I write Platformer, a newsletter about Big Tech and democracy."
      }
    ]
  }
}

List Posts

Retrieve a paginated list of posts from a publication. Returns post metadata without full body content.

GET/v1/publications/:slug/posts

Parameters

NameInTypeRequiredDescription
slugpathstringYesPublication slug
offsetqueryintegerNoNumber of posts to skip (default: 0)
limitqueryintegerNoNumber of posts to return (max 50) (default: 10)

Example Request

bash
curl "https://stackhooks.com/v1/publications/platformer/posts?limit=2" \
  -H "Authorization: Bearer sk_live_abc123..."

Example Response

json
{
  "data": [
    {
      "id": 150847302,
      "slug": "why-meta-is-going-all-in-on-ai",
      "title": "Why Meta is going all in on AI",
      "subtitle": "The company is betting its future on artificial intelligence",
      "publication_slug": "platformer",
      "date": "2025-03-15T12:00:00.000Z",
      "url": "https://www.platformer.news/p/why-meta-is-going-all-in-on-ai",
      "cover_image": "https://substackcdn.com/image/...",
      "word_count": 2450,
      "like_count": 342,
      "comment_count": 87,
      "is_paid": false,
      "authors": [
        {
          "id": 2013012,
          "name": "Casey Newton",
          "handle": "caseynewton",
          "photo_url": "...",
          "bio": "..."
        }
      ]
    }
  ],
  "pagination": {
    "offset": 0,
    "limit": 2,
    "has_more": true
  }
}

Get Post by URL

Retrieve a full post by its Substack URL. Supports both substack.com subdomains and custom domains. Returns full HTML/text body with paywall detection.

GET/v1/posts/by-url

Parameters

NameInTypeRequiredDescription
urlquerystringYesFull Substack post URL (e.g. "https://platformer.substack.com/p/why-meta-is-going-all-in")

Example Request

bash
curl "https://stackhooks.com/v1/posts/by-url?url=https://platformer.substack.com/p/why-meta-is-going-all-in" \
  -H "Authorization: Bearer sk_live_abc123..."

Example Response

json
{
  "data": {
    "id": 150847302,
    "slug": "why-meta-is-going-all-in-on-ai",
    "title": "Why Meta is going all in on AI",
    "subtitle": "The company is betting its future on artificial intelligence",
    "publication_slug": "platformer",
    "date": "2025-03-15T12:00:00.000Z",
    "url": "https://www.platformer.news/p/why-meta-is-going-all-in-on-ai",
    "is_paid": true,
    "word_count": 2450,
    "body_html": "<div class=\"body markup\">...</div>",
    "body_text": "Full plaintext content of the post...",
    "free_text": "Content visible to all readers...",
    "paid_text": "Content behind the paywall..."
  }
}

Get Post

Retrieve a full post by its slug and publication. Returns full HTML/text body with automatic free/paid content splitting.

GET/v1/posts/:slug

Parameters

NameInTypeRequiredDescription
slugpathstringYesPost slug
publicationquerystringYesPublication slug

Example Request

bash
curl "https://stackhooks.com/v1/posts/why-meta-is-going-all-in?publication=platformer" \
  -H "Authorization: Bearer sk_live_abc123..."

Example Response

json
{
  "data": {
    "id": 150847302,
    "slug": "why-meta-is-going-all-in-on-ai",
    "title": "Why Meta is going all in on AI",
    "publication_slug": "platformer",
    "is_paid": true,
    "body_html": "<div class=\"body markup\">...</div>",
    "body_text": "Full plaintext content...",
    "free_text": "Content visible to all readers...",
    "paid_text": "Content behind the paywall..."
  }
}

List Authors

Retrieve a deduplicated list of all authors who have published posts on a publication.

GET/v1/publications/:slug/authors

Parameters

NameInTypeRequiredDescription
slugpathstringYesPublication slug

Example Request

bash
curl https://stackhooks.com/v1/publications/platformer/authors \
  -H "Authorization: Bearer sk_live_abc123..."

Example Response

json
{
  "data": [
    {
      "id": 2013012,
      "name": "Casey Newton",
      "handle": "caseynewton",
      "photo_url": "https://substackcdn.com/image/...",
      "bio": "I write Platformer, a newsletter about Big Tech and democracy."
    }
  ]
}

Get Comments

Retrieve all comments on a post, including nested reply threads. Comments are returned as a recursive tree.

GET/v1/posts/:slug/comments

Parameters

NameInTypeRequiredDescription
slugpathstringYesPost slug
publicationquerystringYesPublication slug

Example Request

bash
curl "https://stackhooks.com/v1/posts/why-meta-is-going-all-in/comments?publication=platformer" \
  -H "Authorization: Bearer sk_live_abc123..."

Example Response

json
{
  "data": [
    {
      "id": 98765432,
      "body_html": "<p>Great analysis!</p>",
      "body_text": "Great analysis!",
      "date": "2025-03-15T14:30:00.000Z",
      "author_name": "Jane Doe",
      "author_photo_url": "https://substackcdn.com/image/...",
      "like_count": 12,
      "children": [
        {
          "id": 98765433,
          "body_html": "<p>Agreed, very insightful.</p>",
          "body_text": "Agreed, very insightful.",
          "date": "2025-03-15T15:00:00.000Z",
          "author_name": "John Smith",
          "author_photo_url": "https://substackcdn.com/image/...",
          "like_count": 3,
          "children": []
        }
      ]
    }
  ]
}

Types

TypeScript-style definitions of all response objects.

Publication

typescript
interface Publication {
  slug: string;
  name: string;
  description: string;
  base_url: string;
  logo_url: string;
  hero_image_url: string;
  twitter_handle: string;
  author_count: number;
  authors: Author[];
}

Post

typescript
interface Post {
  id: number;
  slug: string;
  title: string;
  subtitle: string;
  publication_slug: string;
  date: string;              // ISO 8601
  url: string;
  cover_image: string;
  word_count: number;
  like_count: number;
  comment_count: number;
  is_paid: boolean;
  authors: Author[];
  // Included on single-post endpoints:
  body_html?: string;        // Full HTML content
  body_text?: string;        // Plaintext content
  free_text?: string;        // Content before paywall
  paid_text?: string;        // Content after paywall
}

Author

typescript
interface Author {
  id: number;
  name: string;
  handle: string;
  photo_url: string;
  bio: string;
}

Comment

typescript
interface Comment {
  id: number;
  body_html: string;
  body_text: string;
  date: string;              // ISO 8601
  author_name: string;
  author_photo_url: string;
  like_count: number;
  children: Comment[];       // Recursive nested replies
}

Errors

Error responses include a code and human-readable message.

CodeStatusDescription
unauthorized401Missing or invalid API key
invalid_request400Missing required parameter or malformed input
not_found404Publication, post, or resource not found
rate_limited429Per-minute or daily rate limit exceeded
upstream_error502Substack returned an unexpected error
internal_error500Unexpected server error
json
{
  "error": {
    "code": "not_found",
    "message": "Fetch publication homepage nonexistent: 404 Not Found"
  }
}

Rate Limits

Rate limits are applied per API key. Exceeding limits returns a 429 response. Every response includes rate limit headers.

PlanRequests / minRequests / day
Free10100
Pro605,000
Business20050,000

Response Headers

Every /v1/* response includes these headers:

http
X-RateLimit-Limit-Minute: 10
X-RateLimit-Remaining-Minute: 7
X-RateLimit-Limit-Day: 100
X-RateLimit-Remaining-Day: 93