Dev.to WebDev 🛠 Dev 👁 0 📖 3 min read

Writing cards straight into Anki from a browser extension (AnkiConnect, without the hand-waving)

If you have ever built a tool that generates flashcards, you have probably stopped at the export step: produce a CSV or an APKG, hand it to the user, and let them import it into Anki themselves. That works, but it leaks

If you have ever built a tool that generates flashcards, you have probably stopped at the export step: produce a CSV or an APKG, hand it to the user, and let them import it into Anki themselves. That works, but it leaks users at every hop — the download, the import dialog, the field mapping, and the duplicate pile-up when they run your tool twice.

There is a better option, and it is an add-on most people have heard of but few have wired up: AnkiConnect. This post is the practical version of what that integration actually looks like from inside a browser extension — the parts the README does not cover.

What AnkiConnect actually is

AnkiConnect (add-on code 2055492159) runs a small local HTTP server inside desktop Anki, bound to 127.0.0.1:8765. It speaks JSON: you POST a payload like:

{
  "action": "addNote",
  "version": 6,
  "params": {
    "note": {
      "deckName": "Med school::Lectures",
      "modelName": "Basic",
      "fields": { "Front": "What is X?", "Back": "It is Y" },
      "tags": ["lecture-07"]
    }
  }
}

and Anki replies with a note id — or an error if the deck or model does not exist. That is the whole protocol. No cloud, no account, no file.

The part nobody warns you about: CORS

Here is the catch that turns a one-hour integration into a two-day one. AnkiConnect sets Access-Control-Allow-Origin: *, which sounds permissive — but a browser extension's content script is not a web page. Depending on where you issue the fetch from, you may hit mixed-content rules, or the request may go out with the page's origin rather than your extension's.

The reliable pattern is to never fetch from a content script at all. Do it in the service worker (or background page), where you can use the extension's host permissions to talk to 127.0.0.1 directly:

{
  "host_permissions": ["http://127.0.0.1:8765/*"]
}

Then the content script just passes card data over chrome.runtime.sendMessage, and the worker makes the call. One message channel, one place where networking happens, no CORS surprises.

Duplicates: the problem import never solves

The silent killer of generated decks is the second run. User converts chapter 3, imports, studies. Two weeks later they re-run it with better settings — and now every card exists twice, because a file has no idea what is already in the collection.

AnkiConnect gives you the pieces to fix this, but you have to assemble them. The two calls that matter:

  • findNotes with a query like "deck:Lectures", which returns note ids you can then fetch with notesInfo
  • canAddNotes, which takes a batch of note payloads and tells you which ones would be duplicates of existing notes

The cheap and effective approach: hash the normalized front text (lowercase, whitespace-collapsed) and keep the hash in a dedicated field or tag. Before writing, check canAddNotes and skip anything already present. AnkiMate does exactly this — re-running the same source updates instead of duplicating, which is the difference between a deck people keep and a deck they quietly abandon.

Things that bite in production

Desktop Anki must be running. This is the whole trade-off of the direct-write approach, and you should say it plainly in your UI instead of letting users discover it through a failed fetch. A graceful "Anki isn't open" state converts better than a console error.

Deck and model names are user-defined. Never hardcode "Basic" — fetch modelNames and deckNames on connect, and let the user pick. A card written to a nonexistent model fails silently in some versions.

Port 8765 is configurable. Users who run other tooling occasionally change it. Persist the port in settings and fail loudly with instructions rather than retrying forever.

The local server is trust boundary. Anything on the user's machine can talk to it, which is why AnkiConnect ships with a webApiKey option. If your extension runs in a shared-machine context, support the key.

Why this matters beyond extensions

The same pattern works for any local tool that wants to talk to Anki: scripts, editors, note apps. The export-file workflow made sense when Anki was closed; with a local API, "generate then import" is a choice, not a requirement. The users who care most — medical students, language learners, anyone with a deck they actually maintain — notice the difference immediately, because the file step is where their workflow used to break.

This is the integration behind AnkiMate, a browser extension that turns web pages, PDFs and YouTube transcripts into Anki cards and writes them in over AnkiConnect. It runs three generations a day free; writing to Anki is unlimited free. Disclosure: I built it — the architecture above is the real one, not the brochure version.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.