Using the API
Decks and cards
A deck holds cards. A card holds one term and everything you know about it. Scheduling is per review mode, so the same card can be easy to recognise and hard to produce.
Decks
A deck has a name and defaults for new cards. defaultLanguage prefills the language on every card added to it, and directions decides which way its cards are asked. Only the name is required.
{
"name": "Italian",
"defaultLanguage": "it",
"directions": "both"
}Listing decks gives you the counts too, so a dashboard needs one call: total is the active cards in the deck and due is how many card states are ready to review now.
Cards
Only deckId and term are required. Send what you have; the rest can be filled later, in the app or by a second call.
| Field | Type | Notes |
|---|---|---|
| deckId | string | Required. Which deck the card lands in. |
| term | string | Required. What the card asks about, as you met it. |
| meaning | string | What it means, in your own language. |
| pronunciation | string | How the term is said: IPA or a plain respelling, unless the Languages page gives the card's language a format of its own, such as kana for Japanese. |
| example | string | One sentence using the term, up to 250 characters as shown: readings and stress marks do not count. The Languages page says which languages carry readings inside it, and in what notation. Keep its translation out of it. |
| exampleTranslation | string | The example in your own language, shown after the answer, up to 250 characters. Left out, Lymi writes one when the card is enriched and the deck allows it. An empty string clears it. Ignored without an example. |
| notes | string | Anything else worth keeping. Markdown: **bold**, *italic*, ++underline++, ~~strikethrough~~, links such as [text](https://example.com) or <https://example.com> (http, https or mailto), and lists starting with - or 1. Anything else, HTML included, shows as typed. |
| hint | string | A short association that leads back to the answer without giving any of it away. Review shows it under the cue: the learner can peek at it before the reveal, which rules out Easy, or show it after. Up to 200 characters. |
| language | string | null | BCP 47 tag. Defaults to the deck's. |
| tags | string[] | Up to 20. A name you already use, in any case, keeps your spelling. |
| source | string | Where it came from: a lesson, a book, a film. |
| directions | "recognition" | "production" | "both" | null | Legacy form of reviewModes. Overrides the deck for this card. |
| reviewModes | { cue, target }[] | null | Cue and target pairs. null follows the deck. Picture cues belong on individual cards. |
| meaningSource | "lesson" | "ai" | "manual" | Where the meaning came from. Send lesson or manual; ai is Lymi's own enrichment. |
| exampleSource | "lesson" | "ai" | "manual" | Where the example came from. |
| exampleTranslationSource | "lesson" | "ai" | "manual" | Where the example's translation came from. |
| pronunciationSource | "lesson" | "ai" | "manual" | Where the pronunciation came from. |
| hintSource | "lesson" | "ai" | "manual" | Where the hint came from. |
| enrich | boolean | Write-only. true asks Lymi to fill the empty fields after the add. Off by default for API keys and MCP clients. |
| enrichmentStatus | "working" | "failed" | null | Read-only. Set while Lymi is filling the card's empty fields, and null once it settles. |
Lymi fills what you leave empty, when asked
A card sent with enrich: true is enriched in the background: the language, and the fields its deck's enrichFields allow that suit the card, written in the learner's meaning language. Without it, a card from an API key or an assistant stays exactly as sent. It fills only fields never set, so any text you send is kept exactly as you sent it, whatever its source, and a field sent or saved as "" stays empty. Each filled field is recorded as "ai" and carries the badge.
While a job is outstanding the card reads enrichmentStatus: "working", and "failed" if it gives up. Poll the card to watch it settle to null. The add itself always succeeds; enrichment never holds it up or fails it.
The example's translation keeps its own exampleTranslationStatus, because Lymi translates whatever example the card ends up with, yours or its own. Change an example Lymi translated and it writes the translation again, even without enrich; a translation you sent stays, and exampleTranslationStale turns true until you send a new one.
Ask again for one card, whether its job failed or it was added before enrichment existed. The rules are the add path's: only empty fields the deck allows, only the card's owner, and a card with nothing left to fill is refused with 400.
curl -X POST "$LYMI_URL/api/cards/0mtoyiymrvqpdz02hlv/enrich" -H "x-api-key: $LYMI_KEY"The same term twice is not an error
Adding a term you already have is skipped and reported, never rejected. You get 200 instead of 201, with the card that already holds the term and the deck it lives in. This makes a script safe to re-run.
{
"status": "skipped",
"term": "la nebbia",
"existing": { "id": "0mtoyiymrvqpdz02hlv", "term": "la nebbia" },
"deckName": "Italian"
}What counts as a duplicate
Two cards collide when their normalised term and language both match, anywhere in your decks, not only in the one you are adding to. Normalising trims the term, collapses runs of whitespace, folds case and applies Unicode NFC.
- Accents are kept. In Italian
pesca(peach) andpèsca(fishing) are two words, and Lymi treats them as two. - Articles are kept.
la nebbiaandnebbiaare different terms. - In Japanese the reading counts too.
上手readじょうず(skilful) and readうわて(a cut above) are two words, and Lymi treats them as two. When either card has no reading, term and language decide. - In Chinese the pinyin counts the same way.
行readháng(a row) and readxíng(to go) are two words. Pinyin sent with tone numbers, such asxing2, is stored with tone marks. Punctuation never counts, and neither does the tone over 一 or 不, sobù shìandbú shìare one word. Pinyin with no tone mark counts as no reading. - In Russian and Ukrainian the stress counts the same way. Send the term as natives write it and the pronunciation with U+0301 over the stressed vowel:
замокstressedза́мок(a castle) andзамо́к(a lock) are two words. A pronunciation that is not the term with marks added, such as IPA, counts as no reading. - A card with no language only collides with other cards that have no language.
- Archived cards do not collide. Adding a term you archived makes a fresh card.
A batch checks itself as well: send the same term twice in one call and the second one is skipped against the first.
Review modes decide what a card asks
Each review mode gets its own schedule, but only one mode of a card comes up on a given day.
recognition- You see the term and recall the meaning. The easier way round.
production- You see the meaning and produce the term. The harder way round.
both- Two schedules for one card, with at most one mode asked each day.
Use reviewModes for cue and target pairs, such as {"cue":"term","target":"meaning"}. A card’s modes override its deck’s. Picture modes belong on individual cards and need a described picture. The older directions field is still accepted. A card without an override follows its deck. Set reviewModes explicitly when creating a deck if you want to choose how its cards are asked.
Reviews are scheduled with FSRS
Lymi uses FSRS with one learning step of ten minutes. A new card state starts at 0 and moves through Learning, Review and Relearning as you grade it. How reviews are scheduled explains the intervals and the order cards come up in.
| state | Means |
|---|---|
| 0 | New. Never reviewed. |
| 1 | Learning. Inside its first step. |
| 2 | Review. Graduated, on a real interval. |
| 3 | Relearning. Forgotten and going through the step again. |
Reading the queue is open to any key. GET /api/review/queue returns today’s cards in the order a review would take them if every grade succeeded, and each item carries next: the four dates each grade would schedule. A client can show “Good · 6 d” with no extra round trip. Add round=forgotten, round=new or round=slipping for one of Today’s rounds; GET /api/review/rounds counts each.
Nothing is deleted
Archiving hides a card or a deck; restoring brings it back with its schedule untouched. Archiving a deck leaves its cards alone: they stop appearing with the deck, and they come back when you restore it.
curl -X POST "$LYMI_URL/api/cards/0mtoyiymrvqpdz02hlv/archive" -H "x-api-key: $LYMI_KEY"
curl -X POST "$LYMI_URL/api/cards/0mtoyiymrvqpdz02hlv/restore" -H "x-api-key: $LYMI_KEY"For many cards at once, send up to 200 ids as cardIds to POST /api/cards/archive or POST /api/cards/restore. A card already in the state you asked for is left alone. PATCH /api/cards/batch edits up to 200 cards, each with its cardId and the fields to change.
The response gives one outcome per card, in order, each with the card’s id. A card that is missing, not yours or refused comes back as an error with a code and a message, and the rest still change. If a save fails partway, the cards it held come back as unavailable with nothing on them changed, so you can send just those again. Add ?response=terse to a batch add or a batch edit to get back only each card’s id and status. A skipped add also keeps its term and the deckId and deckName that already hold it.