Skip to the page

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.

Update instead of re-adding
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.

Terminal
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.

Terminal
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.

Terminal
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.