Catalog npm
PACKAGE EXPLORERView in registry

4chanapi.ts

npm

Typed TypeScript client for the 4chan API

4chanapireact-nativetypescript
Versions collected1
Direct dependencies1From release 0.2.0
Last observed20 Oct, 20:07 UTC

README

Current package documentation

4chanapi.ts

A typed TypeScript client for the 4chan API, designed for React Native.

  • Full TypeScript types for every endpoint
  • Built-in rate limiting (≤ 1 request/sec, per API rules)
  • If-Modified-Since support — returns null on HTTP 304
  • URL helpers for images, thumbnails, flags, and spoilers
  • No Node.js dependencies — uses fetch natively available in React Native

Installation

npm install 4chanapi.ts

Quick Start

import { FourChanClient } from "4chanapi.ts";

const client = new FourChanClient();

const thread = await client.getThread("g", 100000000);
if (thread) {
  const op = thread.posts[0];
  console.log(op.sub);   // thread subject
  console.log(op.com);   // comment (HTML-escaped)
}

API Reference

new FourChanClient()

Creates a client with a built-in rate limiter. All requests are serialised with at least 1 second between dispatches.

const client = new FourChanClient();

getBoards()

Fetches all boards and their settings.

const boards = await client.getBoards();

Response: Board[]

[
  {
    "board": "g",
    "title": "Technology",
    "ws_board": 1,
    "per_page": 15,
    "pages": 10,
    "max_filesize": 4096,
    "max_webm_filesize": 3072,
    "max_comment_chars": 2000,
    "max_webm_duration": 120,
    "bump_limit": 310,
    "image_limit": 150,
    "cooldowns": {
      "threads": 600,
      "replies": 60,
      "images": 60
    },
    "meta_description": "...",
    "is_archived": 1
  }
]

Board fields

FieldTypeDescription
boardstringBoard directory name (e.g. "g", "po")
titlestringHuman-readable board title
ws_board0 | 11 = worksafe
per_pagenumberThreads per index page
pagesnumberTotal number of index pages
max_filesizenumberMax non-webm file size in KB
max_webm_filesizenumberMax webm file size in KB
max_comment_charsnumberMax characters in a comment
max_webm_durationnumberMax webm duration in seconds
bump_limitnumberReplies before thread stops bumping
image_limitnumberMax image replies per thread
cooldownsBoardCooldowns{ threads, replies, images } in seconds
is_archived?0 | 1Archive enabled on this board
country_flags?0 | 1Country flags enabled
user_ids?0 | 1Poster IDs enabled
board_flags?Record<string, string>Map of flag code → name
spoilers?0 | 1Spoiler images enabled
custom_spoilers?numberNumber of custom spoiler variants

getThread(board, threadId, opts?)

Fetches a full thread including all replies.

Returns null if the thread has not changed since opts.ifModifiedSince.

const thread = await client.getThread("po", 570368);

// With If-Modified-Since (returns null on HTTP 304)
const lastFetch = new Date();
const updated = await client.getThread("po", 570368, {
  ifModifiedSince: lastFetch,
});
if (updated === null) {
  console.log("No new posts");
}

Response: Thread | null

{
  "posts": [
    {
      "no": 570368,
      "resto": 0,
      "sticky": 1,
      "now": "01/01/24(Mon)00:00",
      "time": 1704067200,
      "name": "Anonymous",
      "sub": "Welcome to /po/",
      "com": "Paper &amp; origami thread.",
      "tim": 1704067200123,
      "filename": "origami",
      "ext": ".jpg",
      "fsize": 204800,
      "md5": "abc123def456ghi789jkl0==",
      "w": 1200,
      "h": 800,
      "tn_w": 250,
      "tn_h": 166,
      "replies": 42,
      "images": 18,
      "unique_ips": 15,
      "last_modified": 1704099600,
      "semantic_url": "welcome-to-po"
    },
    {
      "no": 570400,
      "resto": 570368,
      "now": "01/01/24(Mon)01:30",
      "time": 1704072600,
      "name": "Anonymous",
      "com": "Nice thread, here&#039;s my latest crane.",
      "tim": 1704072600456,
      "filename": "crane",
      "ext": ".png",
      "fsize": 102400,
      "md5": "xyz789abc012def345ghi6==",
      "w": 800,
      "h": 600,
      "tn_w": 250,
      "tn_h": 187
    }
  ]
}

getCatalog(board, opts?)

Fetches all threads on a board grouped by page, including the most recent reply previews.

const catalog = await client.getCatalog("g");

// With cache check
const catalog = await client.getCatalog("g", { ifModifiedSince: lastFetch });
if (catalog === null) return; // not modified

for (const page of catalog) {
  for (const thread of page.threads) {
    console.log(`[${thread.no}] ${thread.sub ?? "(no subject)"} — ${thread.replies} replies`);
  }
}

Response: CatalogPage[] | null

[
  {
    "page": 1,
    "threads": [
      {
        "no": 100000001,
        "resto": 0,
        "now": "03/29/26(Sun)12:00",
        "time": 1743249600,
        "name": "Anonymous",
        "sub": "Programming thread",
        "com": "Post your projects.",
        "tim": 1743249600789,
        "filename": "code",
        "ext": ".png",
        "w": 1920,
        "h": 1080,
        "tn_w": 250,
        "tn_h": 140,
        "replies": 87,
        "images": 12,
        "omitted_posts": 82,
        "omitted_images": 10,
        "last_modified": 1743260000,
        "semantic_url": "programming-thread",
        "last_replies": [
          {
            "no": 100000088,
            "resto": 100000001,
            "now": "03/29/26(Sun)14:55",
            "time": 1743260100,
            "name": "Anonymous",
            "com": "Just finished my Rust project."
          }
        ]
      }
    ]
  }
]

getThreadList(board)

Fetches a lightweight list of all threads and their last-modified timestamps. Useful for polling — much smaller than the full catalog.

const pages = await client.getThreadList("g");

for (const page of pages) {
  for (const thread of page.threads) {
    console.log(thread.no, thread.last_modified, thread.replies);
  }
}

Response: ThreadListPage[]

[
  {
    "page": 1,
    "threads": [
      { "no": 100000001, "last_modified": 1743260000, "replies": 87 },
      { "no": 100000002, "last_modified": 1743259000, "replies": 12 }
    ]
  },
  {
    "page": 2,
    "threads": [
      { "no": 99999900, "last_modified": 1743240000, "replies": 310 }
    ]
  }
]

getIndex(board, page, opts?)

Fetches a single index page (threads + preview replies). Pages are 1-based.

const indexPage = await client.getIndex("g", 1);
if (indexPage) {
  for (const thread of indexPage) {
    const op = thread.posts[0];
    console.log(op.sub, op.replies);
  }
}

Response: IndexPage | null — an array of { posts: Post[] } objects


getArchive(board)

Fetches the list of archived thread IDs. Returns an empty array for boards without archives.

const archivedIds = await client.getArchive("g");
// [571958, 572866, 54195, ...]

Response: number[]

[571958, 572866, 54195, 12345, 67890]

URL Helpers

getImageUrl(board, tim, ext)

Full-size image URL.

import { getImageUrl } from "4chanapi.ts";

const url = getImageUrl("g", post.tim!, post.ext!);
// "https://i.4cdn.org/g/1743249600789.png"

getThumbnailUrl(board, tim)

Thumbnail URL (always JPEG).

import { getThumbnailUrl } from "4chanapi.ts";

const thumb = getThumbnailUrl("g", post.tim!);
// "https://i.4cdn.org/g/1743249600789s.jpg"

getCountryFlagUrl(countryCode)

Country flag GIF (boards with country_flags enabled).

import { getCountryFlagUrl } from "4chanapi.ts";

const flag = getCountryFlagUrl(post.country!);
// "https://s.4cdn.org/image/country/us.gif"

getBoardFlagUrl(board, flagCode)

Board-specific flag GIF (boards with board_flags enabled).

import { getBoardFlagUrl } from "4chanapi.ts";

const flag = getBoardFlagUrl("pol", post.board_flag!);
// "https://s.4cdn.org/image/flags/pol/EU.gif"

getSpoilerUrl(board, customIndex?)

Spoiler placeholder image. Pass a customIndex (1–10) for boards with custom spoilers.

import { getSpoilerUrl } from "4chanapi.ts";

getSpoilerUrl("b");          // default spoiler
getSpoilerUrl("co", 3);      // custom spoiler #3 for /co/

icons

Static icon URLs as constants.

import { icons } from "4chanapi.ts";

icons.sticky       // https://s.4cdn.org/image/sticky.gif
icons.closed       // https://s.4cdn.org/image/closed.gif
icons.admin        // https://s.4cdn.org/image/adminicon.gif
icons.mod          // https://s.4cdn.org/image/modicon.gif
icons.developer    // https://s.4cdn.org/image/developericon.gif
icons.manager      // https://s.4cdn.org/image/managericon.gif
icons.founder      // https://s.4cdn.org/image/foundericon.gif
icons.fileDeletedOp     // https://s.4cdn.org/image/filedeleted.gif
icons.fileDeletedReply  // https://s.4cdn.org/image/filedeleted-res.gif

Error Handling

All methods throw FourChanApiError on non-200/304 responses (e.g. 404 for a thread that no longer exists).

import { FourChanClient, FourChanApiError } from "4chanapi.ts";

const client = new FourChanClient();

try {
  const thread = await client.getThread("g", 1);
} catch (err) {
  if (err instanceof FourChanApiError) {
    console.error(`HTTP ${err.status} — ${err.url}`);
    if (err.status === 404) {
      // thread was deleted
    }
  }
}

Efficient Polling Pattern

Use getThreadList to detect changes cheaply, then only fetch threads that have actually updated.

const client = new FourChanClient();

let knownThreads = new Map<number, number>(); // threadId → last_modified

async function poll() {
  const pages = await client.getThreadList("g");

  for (const page of pages) {
    for (const entry of page.threads) {
      const prev = knownThreads.get(entry.no);

      if (!prev || prev < entry.last_modified) {
        const thread = await client.getThread("g", entry.no, {
          ifModifiedSince: prev ? new Date(prev * 1000) : undefined,
        });
        if (thread) {
          knownThreads.set(entry.no, entry.last_modified);
          // handle updated thread...
        }
      }
    }
  }
}

// Poll every 30 seconds (the client enforces the per-request 1s rate limit)
setInterval(poll, 30_000);

Post Field Reference

All fields on Post except no, resto, now, time, and name are optional — they only appear when applicable.

FieldTypePresent when
nonumberAlways
restonumberAlways — 0 for OP
timenumberAlways — UNIX timestamp
nowstringAlways — MM/DD/YY(Day)HH:MM
namestringAlways — defaults to "Anonymous"
substringOP only, if subject was set
comstringIf a comment was included
tripstringIf poster used a tripcode
idstringOn boards with user IDs
capcodeCapcodeStaff posts only
countrystringOn boards with country flags
board_flagstringOn boards with board flags
since4passnumberIf poster used 4chan pass option
timnumberIf post has an attachment
filenamestringIf post has an attachment
extstringIf post has an attachment
fsizenumberIf post has an attachment
md5stringIf post has an attachment
w / hnumberIf post has an attachment
tn_w / tn_hnumberIf post has an attachment
spoiler1If file is spoilered
custom_spoilernumberIf board has custom spoilers
filedeleted1If file was deleted
m_img1If mobile-optimised image exists
sticky1OP — if thread is pinned
closed1OP — if thread is locked
bumplimit1OP — if bump limit reached
imagelimit1OP — if image limit reached
repliesnumberOP — total reply count
imagesnumberOP — total image reply count
omitted_postsnumberOP — replies not shown in preview
omitted_imagesnumberOP — image replies not shown in preview
last_modifiednumberOP — UNIX timestamp of last activity
semantic_urlstringOP — SEO slug
unique_ipsnumberOP — unique poster count (live threads)
last_repliesPost[]OP — preview of most recent replies
archived1OP — if thread is archived
archived_onnumberOP — UNIX timestamp of archival
tagstringOP — /f/ flash category

Building

npm run build      # compiles to dist/
npm run typecheck  # type-check only, no output

4chan API Terms of Service

  • Do not use "4chan" in your app name, product, or service name.
  • Do not use the 4chan name, logo, or brand to promote your app.
  • Credit the source as 4chan with a link.
  • Do not claim your app is official.
  • Do not clone 4chan or re-host/repackage the API JSON with ads.

Full terms: https://github.com/4chan/4chan-API

Downloads & popularity

Awaiting observations

No metric observations yet

Download counts and popularity will appear here as the collector receives them.

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
0.2.0Registry latest1 Apr 20261

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

PENDING
honosal/4chanapi.ts

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 20:07 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:f0429cbe186eb70893bd3c03cbb0d0f359b8057bfd8d5763d57669346d861456