{"_rev":"3-ae34dce2ca617b8eae388df7db4c8441","time":{"created":"2026-04-01T07:09:04.886Z","modified":"2026-04-01T07:09:05.209Z","0.1.0":"2026-03-31T01:49:03.027Z","0.2.0":"2026-04-01T07:09:05.030Z"},"_id":"4chanapi.ts","name":"4chanapi.ts","dist-tags":{"latest":"0.2.0"},"versions":{"0.2.0":{"name":"4chanapi.ts","author":{"name":"Honosal","email":"honosal875@proton.me","url":"https://github.com/honosal"},"version":"0.2.0","description":"Typed TypeScript client for the 4chan API","main":"dist/index.js","types":"dist/index.d.ts","scripts":{"build":"tsc","typecheck":"tsc --noEmit","test":"echo \"No tests specified\" && exit 0"},"keywords":["4chan","api","typescript","react-native"],"license":"MIT","repository":{"type":"git","url":"git+https://github.com/honosal/4chanapi.ts.git"},"bugs":{"url":"https://github.com/honosal/4chanapi.ts/issues"},"homepage":"https://github.com/honosal/4chanapi.ts","devDependencies":{"typescript":"^5.4.5"},"_id":"4chanapi.ts@0.2.0","gitHead":"65fd18aa512d5e62ea6b3a305e475a20d2c82d57","_nodeVersion":"20.20.1","_npmVersion":"10.8.2","dist":{"integrity":"sha512-4PPrQo7gbvVA6LzQco9poN+sMnTUYsey+I+d3AqkyFtm981fEHJAdsIftje9734sI9Qesz9JRvMrwZ/nN61Npg==","shasum":"ed10fd85883f98be2fe46e6b3071977704e1ed29","tarball":"https://registry.npmjs.org/4chanapi.ts/-/4chanapi.ts-0.2.0.tgz","fileCount":43,"unpackedSize":52208,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICRUCpDmzeu3nuLJXd6RHcTfjkcYW90fRa3Cae2xBtmnAiEA/xQiUjiq1Nvky+T2N37TUmk4b3kP1mFY/RpSd8wPLxA="}]},"_npmUser":{"name":"honosal","email":"admin@bettomillion.com"},"directories":{},"maintainers":[{"name":"honosal","email":"admin@bettomillion.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/4chanapi.ts_0.2.0_1775027344886_0.010136796278928939"},"_hasShrinkwrap":false}},"maintainers":[{"name":"honosal","email":"admin@bettomillion.com"}],"description":"Typed TypeScript client for the 4chan API","homepage":"https://github.com/honosal/4chanapi.ts","keywords":["4chan","api","typescript","react-native"],"repository":{"type":"git","url":"git+https://github.com/honosal/4chanapi.ts.git"},"author":{"name":"Honosal","email":"honosal875@proton.me","url":"https://github.com/honosal"},"bugs":{"url":"https://github.com/honosal/4chanapi.ts/issues"},"license":"MIT","readme":"# 4chanapi.ts\n\nA typed TypeScript client for the [4chan API](https://github.com/4chan/4chan-API), designed for React Native.\n\n- Full TypeScript types for every endpoint\n- Built-in rate limiting (≤ 1 request/sec, per API rules)\n- `If-Modified-Since` support — returns `null` on HTTP 304\n- URL helpers for images, thumbnails, flags, and spoilers\n- No Node.js dependencies — uses `fetch` natively available in React Native\n\n---\n\n## Installation\n\n```sh\nnpm install 4chanapi.ts\n```\n\n---\n\n## Quick Start\n\n```ts\nimport { FourChanClient } from \"4chanapi.ts\";\n\nconst client = new FourChanClient();\n\nconst thread = await client.getThread(\"g\", 100000000);\nif (thread) {\n  const op = thread.posts[0];\n  console.log(op.sub);   // thread subject\n  console.log(op.com);   // comment (HTML-escaped)\n}\n```\n\n---\n\n## API Reference\n\n### `new FourChanClient()`\n\nCreates a client with a built-in rate limiter. All requests are serialised with at least 1 second between dispatches.\n\n```ts\nconst client = new FourChanClient();\n```\n\n---\n\n### `getBoards()`\n\nFetches all boards and their settings.\n\n```ts\nconst boards = await client.getBoards();\n```\n\n**Response: `Board[]`**\n\n```json\n[\n  {\n    \"board\": \"g\",\n    \"title\": \"Technology\",\n    \"ws_board\": 1,\n    \"per_page\": 15,\n    \"pages\": 10,\n    \"max_filesize\": 4096,\n    \"max_webm_filesize\": 3072,\n    \"max_comment_chars\": 2000,\n    \"max_webm_duration\": 120,\n    \"bump_limit\": 310,\n    \"image_limit\": 150,\n    \"cooldowns\": {\n      \"threads\": 600,\n      \"replies\": 60,\n      \"images\": 60\n    },\n    \"meta_description\": \"...\",\n    \"is_archived\": 1\n  }\n]\n```\n\n**`Board` fields**\n\n| Field | Type | Description |\n|---|---|---|\n| `board` | `string` | Board directory name (e.g. `\"g\"`, `\"po\"`) |\n| `title` | `string` | Human-readable board title |\n| `ws_board` | `0 \\| 1` | 1 = worksafe |\n| `per_page` | `number` | Threads per index page |\n| `pages` | `number` | Total number of index pages |\n| `max_filesize` | `number` | Max non-webm file size in KB |\n| `max_webm_filesize` | `number` | Max webm file size in KB |\n| `max_comment_chars` | `number` | Max characters in a comment |\n| `max_webm_duration` | `number` | Max webm duration in seconds |\n| `bump_limit` | `number` | Replies before thread stops bumping |\n| `image_limit` | `number` | Max image replies per thread |\n| `cooldowns` | `BoardCooldowns` | `{ threads, replies, images }` in seconds |\n| `is_archived?` | `0 \\| 1` | Archive enabled on this board |\n| `country_flags?` | `0 \\| 1` | Country flags enabled |\n| `user_ids?` | `0 \\| 1` | Poster IDs enabled |\n| `board_flags?` | `Record<string, string>` | Map of flag code → name |\n| `spoilers?` | `0 \\| 1` | Spoiler images enabled |\n| `custom_spoilers?` | `number` | Number of custom spoiler variants |\n\n---\n\n### `getThread(board, threadId, opts?)`\n\nFetches a full thread including all replies.\n\nReturns `null` if the thread has not changed since `opts.ifModifiedSince`.\n\n```ts\nconst thread = await client.getThread(\"po\", 570368);\n\n// With If-Modified-Since (returns null on HTTP 304)\nconst lastFetch = new Date();\nconst updated = await client.getThread(\"po\", 570368, {\n  ifModifiedSince: lastFetch,\n});\nif (updated === null) {\n  console.log(\"No new posts\");\n}\n```\n\n**Response: `Thread | null`**\n\n```json\n{\n  \"posts\": [\n    {\n      \"no\": 570368,\n      \"resto\": 0,\n      \"sticky\": 1,\n      \"now\": \"01/01/24(Mon)00:00\",\n      \"time\": 1704067200,\n      \"name\": \"Anonymous\",\n      \"sub\": \"Welcome to /po/\",\n      \"com\": \"Paper &amp; origami thread.\",\n      \"tim\": 1704067200123,\n      \"filename\": \"origami\",\n      \"ext\": \".jpg\",\n      \"fsize\": 204800,\n      \"md5\": \"abc123def456ghi789jkl0==\",\n      \"w\": 1200,\n      \"h\": 800,\n      \"tn_w\": 250,\n      \"tn_h\": 166,\n      \"replies\": 42,\n      \"images\": 18,\n      \"unique_ips\": 15,\n      \"last_modified\": 1704099600,\n      \"semantic_url\": \"welcome-to-po\"\n    },\n    {\n      \"no\": 570400,\n      \"resto\": 570368,\n      \"now\": \"01/01/24(Mon)01:30\",\n      \"time\": 1704072600,\n      \"name\": \"Anonymous\",\n      \"com\": \"Nice thread, here&#039;s my latest crane.\",\n      \"tim\": 1704072600456,\n      \"filename\": \"crane\",\n      \"ext\": \".png\",\n      \"fsize\": 102400,\n      \"md5\": \"xyz789abc012def345ghi6==\",\n      \"w\": 800,\n      \"h\": 600,\n      \"tn_w\": 250,\n      \"tn_h\": 187\n    }\n  ]\n}\n```\n\n---\n\n### `getCatalog(board, opts?)`\n\nFetches all threads on a board grouped by page, including the most recent reply previews.\n\n```ts\nconst catalog = await client.getCatalog(\"g\");\n\n// With cache check\nconst catalog = await client.getCatalog(\"g\", { ifModifiedSince: lastFetch });\nif (catalog === null) return; // not modified\n\nfor (const page of catalog) {\n  for (const thread of page.threads) {\n    console.log(`[${thread.no}] ${thread.sub ?? \"(no subject)\"} — ${thread.replies} replies`);\n  }\n}\n```\n\n**Response: `CatalogPage[] | null`**\n\n```json\n[\n  {\n    \"page\": 1,\n    \"threads\": [\n      {\n        \"no\": 100000001,\n        \"resto\": 0,\n        \"now\": \"03/29/26(Sun)12:00\",\n        \"time\": 1743249600,\n        \"name\": \"Anonymous\",\n        \"sub\": \"Programming thread\",\n        \"com\": \"Post your projects.\",\n        \"tim\": 1743249600789,\n        \"filename\": \"code\",\n        \"ext\": \".png\",\n        \"w\": 1920,\n        \"h\": 1080,\n        \"tn_w\": 250,\n        \"tn_h\": 140,\n        \"replies\": 87,\n        \"images\": 12,\n        \"omitted_posts\": 82,\n        \"omitted_images\": 10,\n        \"last_modified\": 1743260000,\n        \"semantic_url\": \"programming-thread\",\n        \"last_replies\": [\n          {\n            \"no\": 100000088,\n            \"resto\": 100000001,\n            \"now\": \"03/29/26(Sun)14:55\",\n            \"time\": 1743260100,\n            \"name\": \"Anonymous\",\n            \"com\": \"Just finished my Rust project.\"\n          }\n        ]\n      }\n    ]\n  }\n]\n```\n\n---\n\n### `getThreadList(board)`\n\nFetches a lightweight list of all threads and their last-modified timestamps. Useful for polling — much smaller than the full catalog.\n\n```ts\nconst pages = await client.getThreadList(\"g\");\n\nfor (const page of pages) {\n  for (const thread of page.threads) {\n    console.log(thread.no, thread.last_modified, thread.replies);\n  }\n}\n```\n\n**Response: `ThreadListPage[]`**\n\n```json\n[\n  {\n    \"page\": 1,\n    \"threads\": [\n      { \"no\": 100000001, \"last_modified\": 1743260000, \"replies\": 87 },\n      { \"no\": 100000002, \"last_modified\": 1743259000, \"replies\": 12 }\n    ]\n  },\n  {\n    \"page\": 2,\n    \"threads\": [\n      { \"no\": 99999900, \"last_modified\": 1743240000, \"replies\": 310 }\n    ]\n  }\n]\n```\n\n---\n\n### `getIndex(board, page, opts?)`\n\nFetches a single index page (threads + preview replies). Pages are 1-based.\n\n```ts\nconst indexPage = await client.getIndex(\"g\", 1);\nif (indexPage) {\n  for (const thread of indexPage) {\n    const op = thread.posts[0];\n    console.log(op.sub, op.replies);\n  }\n}\n```\n\n**Response: `IndexPage | null`** — an array of `{ posts: Post[] }` objects\n\n---\n\n### `getArchive(board)`\n\nFetches the list of archived thread IDs. Returns an empty array for boards without archives.\n\n```ts\nconst archivedIds = await client.getArchive(\"g\");\n// [571958, 572866, 54195, ...]\n```\n\n**Response: `number[]`**\n\n```json\n[571958, 572866, 54195, 12345, 67890]\n```\n\n---\n\n## URL Helpers\n\n### `getImageUrl(board, tim, ext)`\n\nFull-size image URL.\n\n```ts\nimport { getImageUrl } from \"4chanapi.ts\";\n\nconst url = getImageUrl(\"g\", post.tim!, post.ext!);\n// \"https://i.4cdn.org/g/1743249600789.png\"\n```\n\n### `getThumbnailUrl(board, tim)`\n\nThumbnail URL (always JPEG).\n\n```ts\nimport { getThumbnailUrl } from \"4chanapi.ts\";\n\nconst thumb = getThumbnailUrl(\"g\", post.tim!);\n// \"https://i.4cdn.org/g/1743249600789s.jpg\"\n```\n\n### `getCountryFlagUrl(countryCode)`\n\nCountry flag GIF (boards with `country_flags` enabled).\n\n```ts\nimport { getCountryFlagUrl } from \"4chanapi.ts\";\n\nconst flag = getCountryFlagUrl(post.country!);\n// \"https://s.4cdn.org/image/country/us.gif\"\n```\n\n### `getBoardFlagUrl(board, flagCode)`\n\nBoard-specific flag GIF (boards with `board_flags` enabled).\n\n```ts\nimport { getBoardFlagUrl } from \"4chanapi.ts\";\n\nconst flag = getBoardFlagUrl(\"pol\", post.board_flag!);\n// \"https://s.4cdn.org/image/flags/pol/EU.gif\"\n```\n\n### `getSpoilerUrl(board, customIndex?)`\n\nSpoiler placeholder image. Pass a `customIndex` (1–10) for boards with custom spoilers.\n\n```ts\nimport { getSpoilerUrl } from \"4chanapi.ts\";\n\ngetSpoilerUrl(\"b\");          // default spoiler\ngetSpoilerUrl(\"co\", 3);      // custom spoiler #3 for /co/\n```\n\n### `icons`\n\nStatic icon URLs as constants.\n\n```ts\nimport { icons } from \"4chanapi.ts\";\n\nicons.sticky       // https://s.4cdn.org/image/sticky.gif\nicons.closed       // https://s.4cdn.org/image/closed.gif\nicons.admin        // https://s.4cdn.org/image/adminicon.gif\nicons.mod          // https://s.4cdn.org/image/modicon.gif\nicons.developer    // https://s.4cdn.org/image/developericon.gif\nicons.manager      // https://s.4cdn.org/image/managericon.gif\nicons.founder      // https://s.4cdn.org/image/foundericon.gif\nicons.fileDeletedOp     // https://s.4cdn.org/image/filedeleted.gif\nicons.fileDeletedReply  // https://s.4cdn.org/image/filedeleted-res.gif\n```\n\n---\n\n## Error Handling\n\nAll methods throw `FourChanApiError` on non-200/304 responses (e.g. 404 for a thread that no longer exists).\n\n```ts\nimport { FourChanClient, FourChanApiError } from \"4chanapi.ts\";\n\nconst client = new FourChanClient();\n\ntry {\n  const thread = await client.getThread(\"g\", 1);\n} catch (err) {\n  if (err instanceof FourChanApiError) {\n    console.error(`HTTP ${err.status} — ${err.url}`);\n    if (err.status === 404) {\n      // thread was deleted\n    }\n  }\n}\n```\n\n---\n\n## Efficient Polling Pattern\n\nUse `getThreadList` to detect changes cheaply, then only fetch threads that have actually updated.\n\n```ts\nconst client = new FourChanClient();\n\nlet knownThreads = new Map<number, number>(); // threadId → last_modified\n\nasync function poll() {\n  const pages = await client.getThreadList(\"g\");\n\n  for (const page of pages) {\n    for (const entry of page.threads) {\n      const prev = knownThreads.get(entry.no);\n\n      if (!prev || prev < entry.last_modified) {\n        const thread = await client.getThread(\"g\", entry.no, {\n          ifModifiedSince: prev ? new Date(prev * 1000) : undefined,\n        });\n        if (thread) {\n          knownThreads.set(entry.no, entry.last_modified);\n          // handle updated thread...\n        }\n      }\n    }\n  }\n}\n\n// Poll every 30 seconds (the client enforces the per-request 1s rate limit)\nsetInterval(poll, 30_000);\n```\n\n---\n\n## Post Field Reference\n\nAll fields on `Post` except `no`, `resto`, `now`, `time`, and `name` are optional — they only appear when applicable.\n\n| Field | Type | Present when |\n|---|---|---|\n| `no` | `number` | Always |\n| `resto` | `number` | Always — `0` for OP |\n| `time` | `number` | Always — UNIX timestamp |\n| `now` | `string` | Always — `MM/DD/YY(Day)HH:MM` |\n| `name` | `string` | Always — defaults to `\"Anonymous\"` |\n| `sub` | `string` | OP only, if subject was set |\n| `com` | `string` | If a comment was included |\n| `trip` | `string` | If poster used a tripcode |\n| `id` | `string` | On boards with user IDs |\n| `capcode` | `Capcode` | Staff posts only |\n| `country` | `string` | On boards with country flags |\n| `board_flag` | `string` | On boards with board flags |\n| `since4pass` | `number` | If poster used 4chan pass option |\n| `tim` | `number` | If post has an attachment |\n| `filename` | `string` | If post has an attachment |\n| `ext` | `string` | If post has an attachment |\n| `fsize` | `number` | If post has an attachment |\n| `md5` | `string` | If post has an attachment |\n| `w` / `h` | `number` | If post has an attachment |\n| `tn_w` / `tn_h` | `number` | If post has an attachment |\n| `spoiler` | `1` | If file is spoilered |\n| `custom_spoiler` | `number` | If board has custom spoilers |\n| `filedeleted` | `1` | If file was deleted |\n| `m_img` | `1` | If mobile-optimised image exists |\n| `sticky` | `1` | OP — if thread is pinned |\n| `closed` | `1` | OP — if thread is locked |\n| `bumplimit` | `1` | OP — if bump limit reached |\n| `imagelimit` | `1` | OP — if image limit reached |\n| `replies` | `number` | OP — total reply count |\n| `images` | `number` | OP — total image reply count |\n| `omitted_posts` | `number` | OP — replies not shown in preview |\n| `omitted_images` | `number` | OP — image replies not shown in preview |\n| `last_modified` | `number` | OP — UNIX timestamp of last activity |\n| `semantic_url` | `string` | OP — SEO slug |\n| `unique_ips` | `number` | OP — unique poster count (live threads) |\n| `last_replies` | `Post[]` | OP — preview of most recent replies |\n| `archived` | `1` | OP — if thread is archived |\n| `archived_on` | `number` | OP — UNIX timestamp of archival |\n| `tag` | `string` | OP — `/f/` flash category |\n\n---\n\n## Building\n\n```sh\nnpm run build      # compiles to dist/\nnpm run typecheck  # type-check only, no output\n```\n\n---\n\n## 4chan API Terms of Service\n\n- Do not use \"4chan\" in your app name, product, or service name.\n- Do not use the 4chan name, logo, or brand to promote your app.\n- Credit the source as 4chan with a link.\n- Do not claim your app is official.\n- Do not clone 4chan or re-host/repackage the API JSON with ads.\n\nFull terms: [https://github.com/4chan/4chan-API](https://github.com/4chan/4chan-API)\n","readmeFilename":"README.md"}