Anki's own engine as a fast, headless server and command-line tool.
A drop-in replacement for AnkiConnect that doesn't need
Anki Desktop, a display, or a container running a GUI.
pip install git+https://github.com/Macro002/anki-cli
anki-cli serve # AnkiConnect-compatible API on http://127.0.0.1:8765Anki is a Rust core with Python bindings, published by the Anki project as the
anki package — the same engine Anki Desktop runs.
anki-cli puts that engine behind the AnkiConnect protocols and a CLI, so your
collection, scheduler (FSRS and SM-2), card templates, media and AnkiWeb sync work
exactly as in Anki, at a fraction of the cost of running the desktop app.
AnkiConnect is an add-on: every request goes through a running Anki Desktop window. On a server that means a virtual display, Qt and the full desktop app per collection, and GUI-thread round trips on every call. anki-cli calls the engine directly:
| AnkiConnect in Anki Desktop | anki-cli | |
|---|---|---|
| Needs | Anki Desktop running (display/VNC on servers) | Python and the anki package |
| Memory | the whole desktop app per collection | ~65 MB for the server with a collection open |
| A review step (current card, show answer, answer) | 3–4 calls through the GUI thread | ~4 ms total over HTTP |
| Collections per process | one (the open profile) | many, served in parallel |
| Sync, backups, FSRS, filtered decks over the API | partly / via GUI | yes (61 extension methods) |
- Every AnkiConnect legacy action (122), with the same parameters, coercion rules,
results and historical error strings —
addNote,cardsInfo,multi,guiDeckReview,storeMediaFile… Existing clients (Yomitan, Obsidian_to_Anki, asbplayer, GameSentenceMiner, Jiten, LunaTranslator …) work unchanged. - The AnkiConnect v1 JSON-RPC API (95 methods) at
/api/v1, with typed records, pagination cursors, state tokens, batches and structured errors. - 61 extensions: AnkiWeb or self-hosted sync (login, full sync with an explicit
side), backups and restore (
.colpkg), deck tree with due counts in one call, filtered decks and Custom Study, FSRS optimizer and simulator, card info, bury/reposition, CSV import/export, empty cards, media check, undo/redo, config and preferences, profiles, a headless reviewer and browser. See docs/methods.md. - A CLI for humans and scripts:
decks,add,find,show,study(in the terminal, with Anki's scheduler),import,export,sync,backup,stats,check,profiles, andcallfor any API method. - A full-screen terminal app (
anki-cli tui): deck tree, reviewer with audio, browser, note editor, statistics, sync and profiles. Works locally or against a server, including over SSH. - A Python client (
ankicli.client.Client) with keep-alive connections.
pip install git+https://github.com/Macro002/anki-cli # Python 3.10+Linux (x86_64/arm64), macOS and Windows are supported wherever the anki wheel is.
Docker
docker build -t anki-cli .
docker run -d -p 127.0.0.1:8765:8765 -v anki-data:/data -e ANKICLI_API_KEY=change-me anki-clior docker compose up -d with the included docker-compose.yml.
A hardened systemd unit is in contrib/anki-cli.service.
anki-cli serve # foreground; Ctrl-C closes collections cleanlyLegacy (AnkiConnect) protocol:
curl -s localhost:8765 -d '{"action":"addNote","version":6,"params":{"note":{
"deckName":"Default","modelName":"Basic","fields":{"Front":"le chat","Back":"the cat"}}}}'
# {"result": 1793043556528, "error": null}v1 JSON-RPC:
curl -s localhost:8765/api/v1 -d '{"jsonrpc":"2.0","id":1,"method":"decks.tree"}'CLI:
anki-cli add --deck French Front="le chien" Back="the dog"
anki-cli decks
anki-cli find "deck:French is:due"
anki-cli study French # review in the terminal
anki-cli call notes.search query="tag:verb" limit=20
anki-cli call guiDeckReview name=French --legacyWhen a server is running, the CLI talks to it automatically; otherwise it opens the
collection directly (--url and --base choose explicitly).
Python:
from ankicli.client import Client
anki = Client("http://127.0.0.1:8765")
note = anki.call("notes.create", noteType={"name": "Basic"}, deck={"name": "French"},
fields={"Front": "la maison", "Back": "the house"})
state = anki.call("reviewer.start", deck={"name": "French"})
anki.legacy("findCards", query="deck:French")pip install "ankicli[tui] @ git+https://github.com/Macro002/anki-cli"
anki-cli tui # opens the collection, or connects to a running server
anki-cli tui --url http://server:8765 --key …- Decks: the tree with Anki's new / learning / due counts (daily limits applied); create, rename and delete decks.
- Reviewer: Anki's scheduler and its real interval labels. Press space to
show the answer, 1–4 to rate, and u to undo. You can
also edit, flag, suspend and bury from here. Card audio plays through
mpv,ffplay,afplayorpaplay. - Browser: Anki search syntax, a sortable card table with flag and suspended colors, and a live preview. Suspend, flag, move, edit or delete without leaving it.
- Editor: adding uses a form for the note type with live duplicate warnings, and sticky fields are kept between notes.
- Statistics: today's numbers, card states, a year of reviews as a heatmap, and the due forecast, for one deck or the whole collection.
- Sync and profiles: log in to AnkiWeb or a self-hosted server and sync. When a full sync is needed you choose the side explicitly. You can also switch, create, rename and delete profiles.
Press ? anywhere for every key. Cards are shown as text: formatting, cloze,
furigana (猫(ねこ)) and [image: …] placeholders. Styling from the deck's CSS is
not applied.
anki-cli keeps profiles in a folder laid out like Anki Desktop's
(<base>/<profile>/collection.anki2 + collection.media/), by default
~/.local/share/anki-cli (--base or $ANKICLI_BASE to change).
- Use your existing Anki data:
anki-cli serve --base ~/.local/share/Anki2(macOS:~/Library/Application Support/Anki2, Windows:%APPDATA%\Anki2). Close Anki Desktop first — a collection can only be open in one program, and anki-cli says so clearly if it isn't. - A single file:
--collection path/to/collection.anki2. - Many collections in one server: prefix any route with
/profiles/<name>/(e.g.POST /profiles/Alice/api/v1) or sendX-Anki-Profile: Alice. Each profile has its own worker thread, so collections are served in parallel. AnkiConnect'sgetProfiles/loadProfileand v1profiles.*work on the active profile as usual.
anki-cli serve also serves the screens Anki Desktop builds as web pages, so web apps
can show them unchanged: the Statistics screen, Deck Options (presets, every
option, FSRS optimize and simulate), Card Info and the congrats screen.
GET [/profiles/<name>]/pages/graphs[?deck=<deck id> | ?search=<search>]
GET [/profiles/<name>]/pages/deck-options/<deck id>
GET [/profiles/<name>]/pages/card-info/<card id>
GET [/profiles/<name>]/pages/congrats
Open one in a browser with ?key=<API key> (it's exchanged for a cookie), or proxy it
with Authorization: Bearer … and announce the public path with X-Forwarded-Prefix.
The Statistics screen starts on deck:current like in Anki Desktop; ?deck= selects that
deck first, ?search= starts on any search (empty for the whole collection). Add
?theme=dark or #night for dark mode (otherwise the system setting). Embedded in
an iframe, a page reports deckOptionsSaved and deckOptionsClose to its parent with
postMessage.
The pages ship in Anki's GUI package (aqt), which needs Qt. anki-cli instead downloads
the aqt wheel matching your anki version from PyPI once, verifies its checksum and
keeps only the web files (about 5 MB, in ~/.cache/anki-cli/pages/). Offline:
anki-cli pages install --wheel aqt-<version>-py3-none-any.whl.
anki-cli sync login # AnkiWeb (or --endpoint https://your-sync-server/)
anki-cli sync # normal sync, media included
anki-cli sync --upload | --download # when a full sync is requiredOnly Anki's session key is stored (in anki-cli.json, readable by the owner only),
never the password. A normal sync never resolves a full-sync conflict on its own: it
reports fullSyncRequired and you choose which side to keep. Over the API: sync.login,
sync.run (mode: auto | upload | download), sync.status; AnkiConnect's sync
action and v1 collection.sync use the stored login. Anki's self-hosted sync server
works too (python -m anki.syncserver).
- Listens on
127.0.0.1by default. On other interfaces, set an API key:anki-cli serve --host 0.0.0.0 --key …(orANKICLI_API_KEY). Legacy requests send it as"key", v1 requests asAuthorization: Bearer …, media URLs as?key=. - Browser origins follow AnkiConnect's rules:
http://localhost,127.0.0.1and browser extensions are allowed by default; add more with--cors https://example.com(or*). There is no permission dialog:requestPermissiongrants allowed origins and denies others. - Media is served from
GET /media/<file>(Range requests supported) for showing cards in web apps; file names can't escape the media folder.
python bench/bench.py — 5,000-note collection, HTTP keep-alive, 4 CPUs:
| Call | median | p95 |
|---|---|---|
version (round trip) |
0.12 ms | 0.14 ms |
deckNames |
0.16 ms | 0.20 ms |
getDeckStats |
1.25 ms | 1.48 ms |
findCards (5,000 matches) |
1.19 ms | 1.48 ms |
cardsInfo (1 card) |
0.31 ms | 0.46 ms |
cardsInfo (50 cards) |
4.90 ms | 5.64 ms |
addNote |
3.19 ms | 3.97 ms |
review step (guiCurrentCard + guiShowAnswer + guiAnswerCard) |
4.08 ms | 5.96 ms |
decks.tree (v1) |
1.92 ms | 2.97 ms |
- 5,000 notes with
addNotes(500 per call): 2.3 s - 8 concurrent clients: ~850 requests/s
- Server memory with the collection open: ~65 MB
Every operation runs on the collection's own thread and commits before the reply (Anki's WAL-mode SQLite). The test suite checks what matters for a server that holds people's study history:
- Compatibility:
tests/compat/run.pyruns AnkiConnect's own test suite — the recorded Yomitan, Obsidian_to_Anki, asbplayer, GameSentenceMiner, Jiten and LunaTranslator request flows, the legacy error baseline captured from the original implementation (every action at versions 2, 4 and 6) and its normalization rules — plus a v1 contract test that validates every result against AnkiConnect's JSON schema: 877 passed, 15 desktop-only cases excluded (details). - Crash safety: the server is
kill -9ed mid-write by four writers; every acknowledged note is there afterwards and SQLite's integrity check passes. - Concurrency: 32 clients add, read and search at once (nothing lost or duplicated); two collections are served in parallel.
- Sync: full upload, full download, incremental sync both ways, media and conflict handling against Anki's official sync server.
- Clean shutdown, locked collections, size limits, CLI and every extension.
pip install -e ".[test]"
pytest # 63 tests, offline
python tests/compat/run.py # AnkiConnect's suite (fetches it at a pinned commit)| anki-cli | AnkiConnect | ubermenchh/anki-cli | anki-connect-server | |
|---|---|---|---|---|
| Engine | official anki package |
Anki Desktop | own SQLite access + fsrs library, or AnkiConnect |
official anki package |
| Runs without Anki Desktop | yes | no | direct mode only | yes |
| AnkiConnect legacy API | all 122 actions | — (reference) | client of it | 49 actions documented |
| AnkiConnect v1 API | all 95 methods | — (reference) | no | no |
| Checked against AnkiConnect's test suite | yes, 877 passed | — | no | no |
| AnkiWeb / self-hosted sync | yes, incl. full-sync choice | via GUI | no | yes |
| Many collections per process | yes | no | no | no |
| CLI | yes | no | yes (rich, with a TUI) | server and media tools |
| Terminal app (TUI) | yes: reviewer, browser, editor, stats, sync | no | yes | no |
Because anki-cli runs Anki's own engine, scheduling (FSRS and SM-2, learning steps, daily limits, burying), template rendering and sync are Anki's — not a reimplementation that can drift from it. Tools that write the database with their own code have to keep up with every Anki release by hand.
anki-cli has no windows, so the GUI actions work on headless equivalents:
guiDeckReview,guiCurrentCard,guiShowAnswer,guiAnswerCard,guiUndoand the v1reviewer.*methods drive a real reviewer session per collection: the head of Anki's v3 queue, answered with the scheduler's own states (daily limits, learning steps and burying behave exactly as in Anki).guiPlayAudio/reviewer.playAudioare no-ops — usecards.answerAudioand play the files yourself.guiBrowse/guiSelectCard/guiSelectedNoteskeep a browser selection;guiAddCards/guiAddNoteSetData/guiEditNotekeep editor drafts, whicheditors.saveturns into notes.guiImportFileneeds a path;guiExitAnki/application.exitstop the server only with--allow-exit.- v1 operations finish before the reply;
operations.getreturns their result at once. scheduling.answerandanswerCardswork on any card, not only the head of the study queue (in Anki's backend they otherwise fail with "not at top of queue").
git clone https://github.com/Macro002/anki-cli && cd anki-cli
python -m venv .venv && .venv/bin/pip install -e ".[test]"
.venv/bin/pytest
python tools/gen_methods_doc.py # regenerate docs/methods.mdLayout: src/ankicli/services/ (one native implementation per method), legacy.py
(AnkiConnect legacy protocol), rpc.py (JSON-RPC v1), server.py (HTTP), host.py
(one thread per open collection), profiles.py, cli.py, client.py.
AGPL-3.0-or-later, because anki-cli builds on Anki's code, which is AGPL. anki-cli is an independent project, not affiliated with Anki or AnkiConnect. AnkiConnect (by Alex Yatskov and contributors) defines the protocols anki-cli implements; its test suite is used for compatibility testing.



