Observed since 20 Oct, 20:07 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Typed TypeScript client for the 4chan API
A typed TypeScript client for the 4chan API, designed for React Native.
If-Modified-Since support — returns null on HTTP 304fetch natively available in React Nativenpm install 4chanapi.ts
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)
}
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
| Field | Type | Description |
|---|---|---|
board | string | Board directory name (e.g. "g", "po") |
title | string | Human-readable board title |
ws_board | 0 | 1 | 1 = worksafe |
per_page | number | Threads per index page |
pages | number | Total number of index pages |
max_filesize | number | Max non-webm file size in KB |
max_webm_filesize | number | Max webm file size in KB |
max_comment_chars | number | Max characters in a comment |
max_webm_duration | number | Max webm duration in seconds |
bump_limit | number | Replies before thread stops bumping |
image_limit | number | Max image replies per thread |
cooldowns | BoardCooldowns | { threads, replies, images } in seconds |
is_archived? | 0 | 1 | Archive enabled on this board |
country_flags? | 0 | 1 | Country flags enabled |
user_ids? | 0 | 1 | Poster IDs enabled |
board_flags? | Record<string, string> | Map of flag code → name |
spoilers? | 0 | 1 | Spoiler images enabled |
custom_spoilers? | number | Number 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 & 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'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]
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/
iconsStatic 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
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
}
}
}
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);
All fields on Post except no, resto, now, time, and name are optional — they only appear when applicable.
| Field | Type | Present when |
|---|---|---|
no | number | Always |
resto | number | Always — 0 for OP |
time | number | Always — UNIX timestamp |
now | string | Always — MM/DD/YY(Day)HH:MM |
name | string | Always — defaults to "Anonymous" |
sub | string | OP only, if subject was set |
com | string | If a comment was included |
trip | string | If poster used a tripcode |
id | string | On boards with user IDs |
capcode | Capcode | Staff posts only |
country | string | On boards with country flags |
board_flag | string | On boards with board flags |
since4pass | number | If poster used 4chan pass option |
tim | number | If post has an attachment |
filename | string | If post has an attachment |
ext | string | If post has an attachment |
fsize | number | If post has an attachment |
md5 | string | If post has an attachment |
w / h | number | If post has an attachment |
tn_w / tn_h | number | If post has an attachment |
spoiler | 1 | If file is spoilered |
custom_spoiler | number | If board has custom spoilers |
filedeleted | 1 | If file was deleted |
m_img | 1 | If mobile-optimised image exists |
sticky | 1 | OP — if thread is pinned |
closed | 1 | OP — if thread is locked |
bumplimit | 1 | OP — if bump limit reached |
imagelimit | 1 | OP — if image limit reached |
replies | number | OP — total reply count |
images | number | OP — total image reply count |
omitted_posts | number | OP — replies not shown in preview |
omitted_images | number | OP — image replies not shown in preview |
last_modified | number | OP — UNIX timestamp of last activity |
semantic_url | string | OP — SEO slug |
unique_ips | number | OP — unique poster count (live threads) |
last_replies | Post[] | OP — preview of most recent replies |
archived | 1 | OP — if thread is archived |
archived_on | number | OP — UNIX timestamp of archival |
tag | string | OP — /f/ flash category |
npm run build # compiles to dist/
npm run typecheck # type-check only, no output
Full terms: https://github.com/4chan/4chan-API
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.2.0Registry latest | 1 Apr 2026 | 1 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 20:07 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:f0429cbe186eb70893bd3c03cbb0d0f359b8057bfd8d5763d57669346d861456