Integration Guide

Reddit API Code Examples: Python, Node.js, TypeScript, Go

Working Reddit API examples in Python, Node.js, TypeScript, Go and Bun: search posts, read subreddits and full comment threads, paginate, handle errors and run requests concurrently.

Written by Alex P.

  • reddit API
  • reddit API python
  • reddit API nodejs
  • reddit API typescript
  • reddit API go
  • bun
  • reddit scraping
  • code examples

Short answer: you can call Reddit data from any language with one HTTP request. FetchLayer’s Reddit API takes a POST with a JSON body and your key, and returns posts, full comment threads, subreddits and user histories as JSON. It needs no Reddit app or OAuth, costs $1.99 per 1,000 requests with 30 free to start, and the same data is available to AI agents over MCP at https://mcp.fetchlayer.dev. Every example below was run against the live API on 4 November 2026.

Jump to your language: Python · Node.js · TypeScript · Go · Bun

How every request works

All five languages make the same call, so learn it once:

  • URL: POST https://api.fetchlayer.dev/reddit/<endpoint>
  • Headers: Authorization: Bearer ss-your-fetchlayer-key and Content-Type: application/json
  • Body: the endpoint’s parameters as JSON, for example {"query": "self-hosted analytics", "sort": "top", "time": "month"}
  • Response: JSON. Lists come back as items, with nextCursor to fetch the next page.

Get a key and export it as FETCHLAYER_API_KEY; every example reads it from there.

The endpoints you’ll use most

EndpointBodyReturns
searchquery, optional subreddit, sort, time, limitPosts matching a keyword
community-postssubreddit, sort (hot, new, top, rising), or createdAfter/createdBeforeA subreddit’s feed or a date range
posturl of the thread, optional depthThe post and its nested comment tree
more-commentsan expandToken from a threadReplies the thread didn’t include
user-posts, user-commentsusernameA user’s history
search-commentsqueryComments matching a keyword

All 16 endpoints and every field are in the Reddit API reference.

Errors are the same everywhere

Every error has the same body, so one handler covers every language:

{"error":"Missing required parameter: query","status":400,"missing":["query"],"code":"invalid_input","retryable":false,"requestId":"d35ba84b-97fe-4524-a417-ad501e04a227"}

Retry only when retryable is true (503 temporarily_unavailable or time_limit, 502 upstream_error, 500 internal_error), with a backoff. Everything else needs a change on your side: 400 fix the request, 401 check the key, 402 top up, 429 slow down. Quote requestId if you contact support. Failed requests are never billed.


Python

Plain requests is enough. This helper retries only what the API marks retryable:

import os
import time
import requests

API = "https://api.fetchlayer.dev/reddit"
HEADERS = {"Authorization": f"Bearer {os.environ['FETCHLAYER_API_KEY']}"}


def call(endpoint, body, attempts=3):
    """POST to an endpoint; retry only what the API marks retryable."""
    for attempt in range(attempts):
        res = requests.post(f"{API}/{endpoint}", json=body, headers=HEADERS, timeout=60)
        if res.ok:
            return res.json()
        err = res.json()
        if not err.get("retryable") or attempt == attempts - 1:
            raise RuntimeError(f"{err['code']}: {err['error']} (request {err['requestId']})")
        time.sleep(2 ** attempt)


# Search posts
results = call("search", {"query": "self-hosted analytics", "sort": "top", "time": "month", "limit": 5})
for post in results["items"]:
    print(f"{post['score']:>5}  r/{post['subreddit']}  {post['title']}")

# A subreddit's newest posts
feed = call("community-posts", {"subreddit": "selfhosted", "sort": "new", "limit": 5})
print(len(feed["items"]), "new posts; next page cursor:", feed["nextCursor"])

# One thread with its comment tree
thread = call("post", {"url": results["items"][0]["permalink"], "depth": 2})
print(thread["title"], "-", thread["commentCountFromTree"], "comments loaded")

Output from the run:

  358  r/selfhosted  What I run - Oct 2026
  351  r/hermesagent  Hermes Agent v0.22.0: The Threshold Release
  289  r/Qwen_AI  Self hosted Qwen 3.8 27b for real work: my 1 month experience
5 new posts; next page cursor: t3_1xnp7cm
What I run - Oct 2026 - 53 comments loaded

Paginating a subreddit

Pass the previous response’s nextCursor as after. Each page holds 25 posts and costs one request:

import os
import requests

API = "https://api.fetchlayer.dev/reddit"
HEADERS = {"Authorization": f"Bearer {os.environ['FETCHLAYER_API_KEY']}"}


def subreddit_posts(subreddit, pages):
    """Yield a subreddit's newest posts, one page (25 posts) per request."""
    after = None
    for _ in range(pages):
        body = {"subreddit": subreddit, "sort": "new", "pages": 1}
        if after:
            body["after"] = after
        data = requests.post(f"{API}/community-posts", json=body, headers=HEADERS, timeout=60).json()
        yield from data["items"]
        after = data.get("nextCursor")
        if not after:
            break


posts = list(subreddit_posts("python", pages=2))
print(len(posts), "posts, ids unique:", len({p["id"] for p in posts}) == len(posts))

This printed 50 posts, ids unique: True. For a date range instead of “the newest N”, pass createdAfter and createdBefore to community-posts.

Coming from PRAW? You don’t have to rewrite anything: PRAW can point at FetchLayer with a config change. See Migrate PRAW to FetchLayer.


Node.js

Node 18+ has fetch built in, so there are no dependencies. AbortSignal.timeout bounds each request, and a small pool keeps a few requests in flight without a library:

const API = 'https://api.fetchlayer.dev/reddit';
const KEY = process.env.FETCHLAYER_API_KEY;

async function call(endpoint, body, { attempts = 3, timeoutMs = 60_000 } = {}) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(`${API}/${endpoint}`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
      signal: AbortSignal.timeout(timeoutMs),
    });
    if (res.ok) return res.json();
    const err = await res.json();
    if (!err.retryable || attempt === attempts - 1) {
      throw new Error(`${err.code}: ${err.error} (request ${err.requestId})`);
    }
    await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
  }
}

// Several subreddits at once, at most 3 requests in flight
async function mapLimit(items, limit, fn) {
  const results = new Array(items.length);
  let next = 0;
  const workers = Array.from({ length: limit }, async () => {
    while (next < items.length) {
      const i = next++;
      results[i] = await fn(items[i]);
    }
  });
  await Promise.all(workers);
  return results;
}

const subreddits = ['node', 'javascript', 'webdev', 'typescript'];
const feeds = await mapLimit(subreddits, 3, (subreddit) =>
  call('community-posts', { subreddit, sort: 'top', time: 'week', limit: 3 }),
);

feeds.forEach((feed, i) => {
  console.log(`r/${subreddits[i]}`);
  for (const post of feed.items) console.log(`  ${post.score}  ${post.title}`);
});

Run it with node reddit.mjs (the file is an ES module, so top-level await works). It printed the top three posts of the week from each subreddit.

Coming from snoowrap or PRAW-style code

snoowrap, the closest JavaScript equivalent to PRAW, hasn’t had a commit since 2023, needs an approved Reddit app, and builds Reddit’s hostnames into its code, so it can’t be pointed elsewhere. If your code already parses Reddit’s own JSON (Listing, t3 children, after cursors), FetchLayer’s PRAW-compatible API returns that same shape over plain HTTP:

const res = await fetch('https://praw.fetchlayer.dev/r/javascript/hot?limit=5', {
  headers: { Authorization: `Bearer ${process.env.FETCHLAYER_API_KEY}` },
});
const listing = await res.json();

for (const { data: post } of listing.data.children) {
  console.log(post.score, post.title);
}
console.log('after:', listing.data.after); // pass as &after= for the next page

It’s read-only, needs no Reddit app, and costs 1 credit per 25 posts. For new code, the REST calls above are simpler, with flat, documented fields. If you’d rather not write the HTTP layer at all, the official @fetchlayer/reddit npm package gives you typed methods for every endpoint.


TypeScript

Type the parts you rely on, and check them at runtime: a TypeScript type describes the shape, it doesn’t validate it.

export {};

interface RedditPost {
  id: string;
  subreddit: string;
  title: string;
  author: string | null;
  score: number;
  commentCount: number;
  permalink: string;
  createdAt: string;
}

interface Listing<T> {
  items: T[];
  itemCount: number;
  nextCursor: string | null;
}

interface ApiError {
  error: string;
  status: number;
  code: string;
  retryable: boolean;
  requestId: string;
}

class FetchLayerError extends Error {
  constructor(public detail: ApiError) {
    super(`${detail.code}: ${detail.error}`);
  }
}

async function call<T>(endpoint: string, body: Record<string, unknown>): Promise<T> {
  const res = await fetch(`https://api.fetchlayer.dev/reddit/${endpoint}`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.FETCHLAYER_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new FetchLayerError((await res.json()) as ApiError);
  return (await res.json()) as T;
}

// Types describe the shape; check the fields you depend on at runtime.
function isPost(value: unknown): value is RedditPost {
  const p = value as RedditPost;
  return typeof p?.id === 'string' && typeof p?.title === 'string' && typeof p?.score === 'number';
}

const listing = await call<Listing<unknown>>('search', { query: 'drizzle orm', sort: 'new', limit: 5 });
const posts = listing.items.filter(isPost);
for (const post of posts) {
  console.log(`${post.createdAt.slice(0, 10)}  r/${post.subreddit}  ${post.title}`);
}

try {
  await call('search', {});
} catch (e) {
  if (e instanceof FetchLayerError) console.log('handled:', e.detail.code, e.detail.retryable);
}

Runs as-is with Bun (bun reddit.ts) and passes tsc --strict with @types/node; for Node, compile it with tsc first. The last lines show an error being caught as a typed FetchLayerError with its code and retryable flag.


Go

The standard library is all you need. A context bounds the whole run, and a sync.WaitGroup fetches several subreddits at once:

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"sync"
	"time"
)

type Post struct {
	ID           string `json:"id"`
	Subreddit    string `json:"subreddit"`
	Title        string `json:"title"`
	Score        int    `json:"score"`
	CommentCount int    `json:"commentCount"`
	Permalink    string `json:"permalink"`
}

type Listing struct {
	Items      []Post  `json:"items"`
	NextCursor *string `json:"nextCursor"`
}

type APIError struct {
	Error     string `json:"error"`
	Code      string `json:"code"`
	Retryable bool   `json:"retryable"`
	RequestID string `json:"requestId"`
}

func call(ctx context.Context, endpoint string, body any, out any) error {
	payload, _ := json.Marshal(body)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		"https://api.fetchlayer.dev/reddit/"+endpoint, bytes.NewReader(payload))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("FETCHLAYER_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		var apiErr APIError
		json.NewDecoder(res.Body).Decode(&apiErr)
		return fmt.Errorf("%s: %s (retryable=%v, request %s)", apiErr.Code, apiErr.Error, apiErr.Retryable, apiErr.RequestID)
	}
	return json.NewDecoder(res.Body).Decode(out)
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
	defer cancel()

	subreddits := []string{"golang", "rust", "programming"}
	listings := make([]Listing, len(subreddits))
	errs := make([]error, len(subreddits))

	var wg sync.WaitGroup
	for i, sub := range subreddits {
		wg.Add(1)
		go func(i int, sub string) {
			defer wg.Done()
			errs[i] = call(ctx, "community-posts",
				map[string]any{"subreddit": sub, "sort": "top", "time": "week", "limit": 3}, &listings[i])
		}(i, sub)
	}
	wg.Wait()

	for i, sub := range subreddits {
		if errs[i] != nil {
			fmt.Println("r/"+sub, "failed:", errs[i])
			continue
		}
		fmt.Println("r/" + sub)
		for _, p := range listings[i].Items {
			fmt.Printf("  %5d  %s\n", p.Score, p.Title)
		}
	}
}

go run main.go printed each subreddit’s top three posts of the week. Swap the WaitGroup for golang.org/x/sync/errgroup if you want the first error to cancel the rest.


Bun

Bun adds two things that make a small Reddit monitor one file: a built-in SQLite driver and a built-in HTTP server. This stores new matches and serves them as JSON:

import { Database } from 'bun:sqlite';

const db = new Database('reddit.sqlite');
db.run(`CREATE TABLE IF NOT EXISTS posts (
  id TEXT PRIMARY KEY, subreddit TEXT, title TEXT, score INTEGER, permalink TEXT, created_at TEXT
)`);
const insert = db.prepare(
  'INSERT OR IGNORE INTO posts (id, subreddit, title, score, permalink, created_at) VALUES (?, ?, ?, ?, ?, ?)',
);

async function poll(query: string) {
  const res = await fetch('https://api.fetchlayer.dev/reddit/search', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${Bun.env.FETCHLAYER_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query, sort: 'new', limit: 25 }),
  });
  if (!res.ok) throw new Error(JSON.stringify(await res.json()));
  const { items } = await res.json();
  let added = 0;
  for (const p of items) {
    added += insert.run(p.id, p.subreddit, p.title, p.score, p.permalink, p.createdAt).changes;
  }
  return added;
}

console.log('new posts stored:', await poll('bun runtime'));

// Serve what you stored
const server = Bun.serve({
  port: 3000,
  fetch() {
    const rows = db.query('SELECT subreddit, title, score, permalink FROM posts ORDER BY created_at DESC LIMIT 50').all();
    return Response.json(rows);
  },
});

const rows = await (await fetch(`http://localhost:${server.port}/`)).json();
console.log('served', rows.length, 'rows; first:', rows[0]?.title);
server.stop();

INSERT OR IGNORE on the post id means re-running it on a schedule only stores what’s new. Run it with bun monitor.ts; on our run it stored 7 new posts and served them back.


What it costs

Every language costs the same: $1.99 per 1,000 requests pay-as-you-go, less on monthly plans, and the first 30 requests are free (pricing).

  • search and community-posts: one request per page of results (25 posts on a feed).
  • post: 1 credit per 2 pages of comments read, so most threads cost 1.
  • Failed requests are never billed.

10,000 subreddit posts are about 400 requests, roughly $0.80.