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

memrise

v0.9.0

Published

Unofficial Memrise API client for Node.js and Bun

Readme

Memrise API Client

Unofficial Memrise Community Courses API client JavaScript

Installation

npm install memrise

Usage

import { MemriseClient } from "memrise";

// Initialize with username and password
const client = new MemriseClient("username", "password");

// Get the signed-in account
const me = await client.getMe();
console.log(me.username, me.is_pro);

// Get your courses
const courses = await client.getMyCourses();
console.log(courses);

// Get course by ID or slug
const course = await client.getCourseById("123456");
const courseBySlug = await client.getCourseBySlug("my-course");

// Get course columns (to see what fields are available)
const columns = await client.getCourseColumns("123456");
console.log(columns);
// { "1": { "kind": "text", "label": "Word", ... }, ... }

// Add items to a course
await client.addThingToCourse("123456", {
  "1": "hello",
  "2": "こんにちは",
});

// Get course levels
const levels = await client.getCourseLevels("123456");

// Get course levels including empty levels visible in the editor
const allLevels = await client.getCourseLevelsIncludingEmpty("123456");

// Get all items from a course
const allItems = await client.getCourseItems("123456");

// Get limited items from a course (first 10 items)
const limitedItems = await client.getCourseItems("123456", 10);

// Get all items from a specific level (level 0)
const levelItems = await client.getLevelItems("123456", 0);

// Get limited items from a specific level (first 5 items from level 1)
const limitedLevelItems = await client.getLevelItems("123456", 1, 5);

// Add items to a specific level
await client.addThingToLevel("level-id", {
  "1": "word",
  "2": "definition",
});

// Bulk add items to a specific level
await client.bulkAddToLevel("level-id", [
  ["word", "definition"],
  ["word2", "definition2"],
]);

// Bulk add to a course (default: first level)
await client.bulkAddToCourse("123456", [
  ["hello", "こんにちは"],
  ["goodbye", "さようなら"],
]);

// Bulk add to the course database only (not attached to a level)
await client.bulkAddToPool("pool-id", [
  { "1": "hello", "2": "こんにちは" },
  { "1": "goodbye", "2": "さようなら" },
]);

// Or pass the raw delimited text the site uses
await client.bulkAddToLevel("level-id", "foo,bar\nfoo2,bar2");

// Add a new level to a course (auto-uses pool from first existing level)
await client.addLevelToCourse("123456");

// Add a new level to a course with explicit pool_id and kind
await client.addLevelToCourse("123456", "7772442", "things");

// Rename a level
await client.setLevelTitle("level-id", "02/19");

// Delete a level
await client.deleteLevel("level-id");

// Set what a level prompts with and what it tests on
await client.setLevelColumnPair("level-id", {
	learningColumn: 1,
	definitionColumn: 2,
});

// Change a pool column's display and testing settings
await client.setPoolColumnSettings("pool-id", "Definition", {
	keyboard: "abc def",
	typingStrict: true,
});

// Edit an existing item, by column name or numeric key
await client.updateThing("thing-id", { Definition: "corrected" });

// Skip the name and pool lookups, and the verifying read-back
await client.updateThing(
  "thing-id",
  { "2": "corrected" },
  { poolId: "pool-id", verify: false },
);

// Or one cell at a time
await client.updateThingCell("thing-id", "2", "corrected");
await client.updateThingCell("thing-id", "Pronunciation", "kon-ni-chi-wa", {
  cellType: "attribute",
});

// Read a single row, every column, by thing ID
const { thing } = await client.getThing("thing-id");

// Take a thing out of a level. The pool row survives, so other levels using
// it are untouched.
await client.detachThingFromLevel("level-id", "thing-id");

// Destroy the pool row itself, removing it from every level at once
await client.deleteThing("thing-id");

// Rows left in the pool that no level uses. Detaching creates these.
const orphans = await client.findOrphanedThings("course-id");

// Or the whole pool, each row with the levels it belongs to
const poolThings = await client.getPoolThings("course-id");

// List every thing in a level, with the thing IDs the delete calls need
const levelThings = await client.getLevelThings("course-id", "level-id");

// Or just the thing IDs, with no extra request for the text
const thingIds = await client.getLevelThingIds("course-id", "level-id");

// Search pool. At least one non-empty column value is required, because Memrise has
// no "return everything" mode, use getLevelThings to enumerate instead.
const results = await client.searchPool("pool-id", {
  "1": "search term",
});

// Get pool information
const pool = await client.getPool("pool-id");

API

Constructor

new MemriseClient(username: string, password: string, clientId?: string)

Methods

Account:

  • getMe() - The signed-in account: id, username, email, locale, Pro status and lifetime statistics

Course Management:

  • getAllMyCourses(limit?) - Every course on your dashboard, following pagination
  • getMyCourses(limit?, offset?) - One raw page of courses (the API caps limit at 9)
  • getCourseById(courseId) - Get course by ID
  • getCourseBySlug(slug) - Get course by slug
  • getCourseLevels(courseId) - Get levels for a course
  • getCourseLevelsIncludingEmpty(courseId, slug?) - Get levels including empty draft levels from the edit view
  • getCourseColumns(courseId) - Get column configuration for a course

Reading Items:

  • getCourseItems(courseId, limit?) - Every item in a course, each tagged with its thingId and the levelIds it belongs to
  • getLevelItems(courseId, levelNumber, limit?) - Get items from a level by its Memrise number (1-based)
  • getLevelByNumber(courseId, levelNumber) - Resolve a level by its Memrise number
  • getLearnable(learnableId) - Get a single learnable item
  • getLearnables(learnableIds) - Fetch many learnables in one batched request
  • getLevel(courseId, levelId) - Look up a single level
  • getLevelThingIds(courseId, levelId) - Thing IDs attached to a level, in level order
  • getLevelThings(courseId, levelId) - Things in a level, each with its thing ID and the learnable text
  • getLevelColumnPair(courseId, levelId) - Which columns a level tests
  • getLearnableIdInLevel(courseId, levelId, thingId) - Learnable ID for a thing in a level, or null

Adding Items:

  • addThingToCourse(courseId, columns, levelNumber?) - Add item to course (default: level 1)
  • addThingToLevel(levelId, columns) - Add item to specific level
  • bulkAddToCourse(courseId, rows, levelNumber?, delimiter?) - Bulk add items to a course (default: level 1)
  • bulkAddToLevel(levelId, rows, delimiter?) - Bulk add items to a specific level
  • bulkAddToPool(poolId, rows, delimiter?) - Bulk add items to a pool (not attached to a level)
  • addLevelToCourse(courseId, poolId?, kind?) - Add a new level to a course
  • setLevelTitle(levelId, newTitle) - Rename a level
  • setLevelColumnPair(levelId, pair) - Set which column the level prompts with (learningColumn) and which it tests on (definitionColumn). Level-scoped, so levels sharing a pool can test different pairs
  • deleteLevel(levelId) - Delete a level
  • detachThingFromLevel(levelId, thingId) - Take a thing out of one level. The pool row survives, so levels sharing it keep it (was deleteThingFromLevel, still available as a deprecated alias)
  • deleteThing(thingId) - Destroy a pool row outright. Removes it from every level at once and cannot be undone

Editing Items:

  • getThing(thingId) - Read one pool row, every column and attribute
  • updateThing(thingId, columns, options?) - Overwrite several cells of one thing, then read the row back to confirm. Options: cellType ("column" by default, or "attribute"), poolId, verify (false skips the read-back)
  • updateThingCell(thingId, cell, newValue, options?) - Overwrite one cell. Options: cellType, poolId

Pool Operations:

  • searchPool(poolId, columns, excludeThingIds?, originalOnly?) - Search pool. Requires at least one non-empty column value; an empty filter throws (the endpoint answers 500)
  • getPool(poolId) - Get pool information, including each column's label and settings
  • setPoolColumnSettings(poolId, column, settings) - Update a column's display and testing settings. Omitted fields keep their current value
  • getPoolIdForLevelId(levelId) - Pool behind a level, without needing the course ID
  • resolveColumnKeys(poolId, row) - Translate column names to numeric keys
  • resolveColumnKey(poolId, column) - Translate one column name to its numeric key
  • getPoolThings(courseId) - Every row in the course's pool, with the levels each belongs to. Scrapes the editor's database pages, the only way to see a pool whole
  • findOrphanedThings(courseId) - Pool rows no level uses

Column names

Memrise stores columns under numeric keys, but you can use their labels:

await client.bulkAddToLevel(levelId, [
  { Word: "หมา (maa)", Definition: "dog" },
  { Word: "แมว (maew)", Definition: "cat" },
]);

Names are matched case-insensitively and resolved against the pool, which is looked up once and cached. Numeric keys still work and skip the lookup entirely, so nothing gets slower:

await client.bulkAddToLevel(levelId, [{ "1": "หมา (maa)", "2": "dog" }]);

A name that does not exist fails with the list of ones that do:

Pool 7778482 has no column named "Wrod". Available columns: word, definition, audio.

Level numbers

Level numbers are the 1-based numbers Memrise shows in the editor, and they are matched against each level's own index:

await client.getLevelItems(courseId, 3);
await client.getLevelByNumber(courseId, 3);

This matters because the levels endpoint omits empty levels while the survivors keep their real numbers. Counting positions in the returned array drifts by however many empty levels precede it. Asking for a number that is empty or missing is an error naming the numbers that do exist, rather than silently returning the wrong level.

Thing IDs and learnable IDs

Memrise exposes two identifiers for what looks like the same item:

  • thing: the row in the pool database. detachThingFromLevel, searchPool and the addThing* responses all speak thing IDs.
  • learnable: a thing plus the pair of columns being tested. Levels and the course-facing APIs report learnable IDs.

A learnable ID packs both together: the thing ID sits in the high bits and the low 16 bits hold the column pair, so 0x...0102 means "column 1 prompts column 2". thingIdFromLearnableId(learnableId) recovers the thing ID without another request, which is how getLevelThingIds works. The reverse direction is not derivable, since it needs the column pair.

This packing is an undocumented implementation detail, so treat it as a fast path rather than a guarantee: it is safe to use for lookups and membership checks, where a wrong answer surfaces as "not found", but never derive an ID for a destructive call that was not confirmed against the API first.

The SDK keeps these apart for you rather than making you remember which is which:

  • Types that carry both. getLevelThings() returns { thingId, learnableId, … }, so you never unpack an ID by hand.
  • The compiler catches mix-ups. ThingId and LearnableId are branded types. They erase to plain numbers at runtime, but passing one where the other belongs is a compile error. Use asThingId() / asLearnableId() to brand a number from elsewhere.
  • So does the runtime. Passing a learnable ID to detachThingFromLevel throws immediately with the thing ID you probably meant, rather than failing somewhere downstream.

Only one direction is derivable without a request:

| Direction | Call | | --- | --- | | learnable → thing | thingIdFromLearnableId(learnableId) | | learnable → columns tested | columnPairFromLearnableId(learnableId) | | thing → learnable | client.getLearnableIdInLevel(courseId, levelId, thingId) |

See docs/api.md for the evidence behind this, the full endpoint reference, and the quirks worth knowing before adding new calls.

License

MIT