npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

wanikani-sdk

v0.2.0

Published

Typed, runtime-validated TypeScript SDK for the WaniKani v2 API.

Readme

wanikani-sdk

npm version CI license

Typed, runtime-validated TypeScript SDK for the WaniKani v2 API.

Schemas are defined with valibot and serve as the single source of truth for both compile-time types and runtime validation of inputs and outputs. Includes a built-in 60 req/min rate limiter and a strict one-retry policy on transient failures to keep API keys out of trouble.

Status: API is pre-1.0; minor versions may introduce breaking changes until 1.0.0.

Install

bun add wanikani-sdk
# or
npm install wanikani-sdk

Requires Node 18+ / Bun / modern browsers / Workers (uses native fetch).

Quick start

import { WanikaniClient } from "wanikani-sdk";

const client = new WanikaniClient({ apiKey: process.env.WANIKANI_API_KEY });

const me = await client.user.get();
console.log(me.data.username, "is level", me.data.level);

for await (const page of client.subjects.paginate({ types: ["kanji"], levels: [1] })) {
  for (const env of page.data) console.log(env.id, env.data.characters);
}

If no apiKey is passed, the client reads WANIKANI_API_KEY from the environment.

Configuration

Every option on new WanikaniClient(options):

new WanikaniClient({
  apiKey: string, // default: process.env.WANIKANI_API_KEY
  revision: "20170710", // default; pin to a docs revision
  baseUrl: string, // default: "https://api.wanikani.com/v2/"
  fetch: typeof fetch, // default: globalThis.fetch
  rateLimit: { rpm: 60 }, // default; set `false` to disable client-side limiting
  validate: "both", // default; "input" | "output" | "none"
});

| Option | What it does | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | apiKey | Personal Access Token for the WaniKani account. Falls back to process.env.WANIKANI_API_KEY; constructor throws synchronously if neither is set. | | revision | Sent as the Wanikani-Revision header. Only change this if you've migrated to a newer docs revision. | | baseUrl | Override for tests or proxies. Trailing slash auto-added. | | fetch | Inject a custom fetch — useful for instrumentation, retries beyond the built-in policy, or running in environments that don't expose a global fetch. | | rateLimit | Token bucket sized to rpm tokens, refilled at rpm/60/sec. false skips the limiter (don't do this against the real API). | | validate | both parses inputs and outputs through valibot. output only checks responses (still gives you types but trusts your inputs). input only checks request bodies. none skips both — fastest, no safety net. |

Retry policy (non-configurable)

A failed request retries at most once, only for clearly transient signals:

  • 429: sleep until RateLimit-Reset, retry once. A second 429 throws WanikaniRateLimitError.
  • 503: 2-second backoff, retry once.
  • Network errors (fetch rejection): retry once.

Deterministic 4xx (401, 403, 404, 409, 422) and 500 are never retried — retrying them risks getting the API key banned.

Resources

Every resource hangs off a configured WanikaniClient instance.

| Resource | GET | POST / PUT | | ----------------------------------------------------- | ------------------------------------------ | ---------------------------------- | | user | .get() | .update(prefs) | | summary | .get() | — | | subjects | .get(id), .list(...), .paginate(...) | — | | assignments | .get(id), .list(...), .paginate(...) | (start: deferred) | | reviewStatistics | .get(id), .list(...), .paginate(...) | — | | studyMaterials | .get(id), .list(...), .paginate(...) | .create(...), .update(id, ...) | | levelProgressions | .get(id), .list(...), .paginate(...) | — | | resets | .get(id), .list(...), .paginate(...) | — | | spacedRepetitionSystems | .get(id), .list(...), .paginate(...) | — | | voiceActors | .get(id), .list(...), .paginate(...) | — |

Roadmap

Known deferrals — these will land in later 0.x releases:

  • POST /reviews (submit a review) and PUT /assignments/:id/start — intentionally not exposed yet to avoid accidentally advancing SRS state on real accounts.
  • Per-resource If-Modified-Since / If-None-Match arguments. Today the transport supports conditional requests end-to-end but the resource methods don't yet take the headers as a parameter; see Conditional requests.
  • A built-in caching layer on top of the conditional-request plumbing.

user

const me = await client.user.get();
console.log(me.data.username, me.data.level, me.data.subscription.type);

// Update preferences (the only mutable user fields)
await client.user.update({
  preferences: { reviews_autoplay_audio: true, lessons_batch_size: 5 },
});

summary

const summary = await client.summary.get();
console.log(summary.data.lessons.length, "lesson buckets ready");
console.log("next reviews at:", summary.data.next_reviews_at);

subjects

// Single subject by id
const kanji = await client.subjects.get(440);

// One page (default per_page is 1000 for subjects)
const radicals = await client.subjects.list({ types: ["radical"], levels: [1, 2] });

// Stream every page
for await (const page of client.subjects.paginate({ types: ["kanji"] })) {
  for (const env of page.data) {
    // env.data is a Radical | Kanji | Vocabulary | KanaVocabulary discriminated by env.object
  }
}

assignments

const review = await client.assignments.list({
  immediately_available_for_review: true,
});
console.log("ready to review:", review.total_count);

reviewStatistics

const weak = await client.reviewStatistics.list({
  percentages_less_than: 60,
  subject_types: ["kanji"],
});

studyMaterials

// Add a synonym + meaning note
const created = await client.studyMaterials.create({
  subject_id: 440,
  meaning_synonyms: ["one (counter)"],
  meaning_note: "Use for counting flat objects.",
});

// Update later
await client.studyMaterials.update(created.id, {
  meaning_synonyms: ["one (counter)", "single"],
});

levelProgressions

const progressions = await client.levelProgressions.list();
console.log("current level started at:", progressions.data.at(-1)?.data.unlocked_at);

resets

const resets = await client.resets.list();
console.log(resets.total_count, "total resets on this account");

spacedRepetitionSystems

const srs = await client.spacedRepetitionSystems.get(1);
for (const stage of srs.data.stages) {
  console.log("stage", stage.position, "→", stage.interval, stage.interval_unit);
}

voiceActors

const actors = await client.voiceActors.list();
for (const env of actors.data) console.log(env.data.name, env.data.gender);

Pagination

WaniKani uses cursor-based pagination (page_after_id / page_before_id). Each .list(...) returns a single page; .paginate(...) returns an AsyncGenerator that walks the pages.next_url chain until exhaustion.

// Eager: collect everything (large for /subjects — ~9000 items)
const all = [];
for await (const page of client.subjects.paginate()) all.push(...page.data);

// Lazy: stop early
for await (const page of client.assignments.paginate({ in_review: true })) {
  if (page.data.length === 0) break;
  process(page.data);
}

per_page is controlled by WaniKani (500 for most resources, 1000 for /subjects and /reviews). The SDK doesn't override it.

Conditional requests

The transport sends If-Modified-Since / If-None-Match when you provide them, and translates a 304 response into a WanikaniNotModified error. ETag handling is plumbed end-to-end — what's missing from v0.1 is wiring through the per-resource methods. Caching is left to the consumer:

import { WanikaniNotModified } from "wanikani-sdk";

try {
  const fresh = await client.subjects.list({ updated_after: lastSyncedAt });
  cache.write(fresh);
} catch (err) {
  if (err instanceof WanikaniNotModified) {
    // Nothing changed since lastSyncedAt — keep using the cache.
  } else throw err;
}

The updated_after query param is the v0.1-supported way to do incremental sync. Native If-Modified-Since per-resource is on the v0.2 roadmap.

Errors

All errors extend WanikaniError:

  • WanikaniApiError — non-2xx HTTP response. Carries status, code, url.
  • WanikaniRateLimitError — 429 after the single allowed retry. Carries the RateLimit-Reset epoch as resetAt: Date | null.
  • WanikaniValidationError — request input or response payload failed valibot validation. Carries direction: "input" | "output" and the raw issues from valibot.
  • WanikaniNotModified — thrown on 304 from conditional requests.
import { WanikaniApiError, WanikaniRateLimitError } from "wanikani-sdk";

try {
  await client.user.get();
} catch (err) {
  if (err instanceof WanikaniRateLimitError) console.warn("retry after", err.resetAt);
  else if (err instanceof WanikaniApiError && err.status === 403) {
    console.error("account hibernating or token revoked");
  } else throw err;
}

Browser usage

WaniKani's API does not advertise CORS for arbitrary browser origins. Call this SDK from a server-side route, edge function, or proxy — not from page JS.

License

ISC