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 an existing card, use PATCH /api/cards/{id} with the id the skip gave you, rather than adding it again.
for (const r of results) {
if (r.status !== "skipped" || r.existing.meaning) continue;
await fetch(`${process.env.LYMI_URL}/api/cards/${r.existing.id}`, {
method: "PATCH",
headers: {
"x-api-key": process.env.LYMI_KEY,
"content-type": "application/json",
},
body: JSON.stringify({ meaning: meaningFor(r.term) }),
});
}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 comes back oldest due first, 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")")'Back a deck up
Cards come back with their scheduling state, so a dump is enough to rebuild the deck elsewhere or to keep a copy outside Lymi.
DECK=0mtoyiymqa34h1xeaqo
curl -s "$LYMI_URL/api/decks/$DECK/cards" -H "x-api-key: $LYMI_KEY" \
> "italian-$(date +%Y-%m-%d).json"Want it as a spreadsheet instead? The deck menu in the app exports a CSV. This route is for the full shape, including fsrs and every timestamp.