Using the API
Recipes
Four things people actually do with the API, written out in full. Each one assumes LYMI_URL and LYMI_KEY are set, as in the Quickstart.
Import a lesson
A lesson is twenty to forty cards. Send them in one batch and read the outcomes: the ones you already had come back as skipped, so you learn what was new without a second call.
import { readFileSync } from "node:fs";
const deckId = "0mtoyiymqa34h1xeaqo";
// One "term,meaning" per line.
const cards = readFileSync("lesson.csv", "utf8")
.split("\n")
.filter(Boolean)
.map((line) => {
const [term, meaning] = line.split(",");
return { deckId, term: term.trim(), meaning: meaning?.trim() };
});
const res = await fetch(`${process.env.LYMI_URL}/api/cards/batch`, {
method: "POST",
headers: {
"x-api-key": process.env.LYMI_KEY,
"content-type": "application/json",
},
body: JSON.stringify({ cards }),
});
const { results } = await res.json();
const added = results.filter((r) => r.status === "added");
console.log(`Added ${added.length}, skipped ${results.length - added.length}`);
for (const r of results) {
if (r.status === "skipped") console.log(` already in ${r.deckName}: ${r.term}`);
}Re-run a script without making a mess
You do not need to check whether a card exists before adding it. The duplicate rule does that for you, in the same call, against every deck you have. Run the same import twice and the second run adds nothing and reports every term as skipped.
Two things follow. Keep your source list as the source of truth, and let Lymi decide what is new. And if you do want to change existing cards, send them to PATCH /api/cards/batch with the ids the skips gave you, rather than adding them again. Each card succeeds or fails on its own; send again only the ones that come back as errors. ?response=terse returns only each card’s id and status, and for a skip the term and the deck that holds it.
const edits = results
.filter((r) => r.status === "skipped" && !r.existing.meaning)
.map((r) => ({ cardId: r.existing.id, meaning: meaningFor(r.term) }));
if (edits.length > 0) {
await fetch(`${process.env.LYMI_URL}/api/cards/batch?response=terse`, {
method: "PATCH",
headers: {
"x-api-key": process.env.LYMI_KEY,
"content-type": "application/json",
},
body: JSON.stringify({ cards: edits }),
});
}See what is due right now
GET /api/decks already carries the counts, so a status line needs one request and no maths.
curl -s "$LYMI_URL/api/decks" -H "x-api-key: $LYMI_KEY" \
| jq -r '.[] | select(.due > 0) | "\(.due)\t\(.name)"'For the cards themselves rather than the counts, ask the queue. It follows the same draw as a review, and total tells you how many there are beyond the page you asked for.
curl -s "$LYMI_URL/api/review/queue?limit=5" -H "x-api-key: $LYMI_KEY" \
| jq -r '"\(.total) due", (.items[] | " \(.card.term) — \(.card.meaning // "no meaning yet")")'Export a deck
Use the export API for a portable copy with media and review history. A read key can start an export. Poll the returned export’s status, then download its downloadUrl with the same key when it is done. The file is available for a day.
curl "$LYMI_URL/api/exports" \
-H "x-api-key: $LYMI_KEY" \
-H "content-type: application/json" \
-d '{"format":"lymi","deckId":"0mtoyiymqa34h1xeaqo"}'The export guide explains formats and ownership limits. The API reference documents polling and downloads. Listing a deck’s cards alone does not include every mode’s state, full review history or media.