TypeScript SDK
The official SDK is the REST API as plain, typed functions: one per endpoint, named after the operation. It sends standard fetch requests, has no dependencies, and leaves out the functions you do not import.
Install#
It runs wherever fetch does: Node 20 and later, Bun, Deno, Cloudflare Workers and browsers. It is an ES module with its own type declarations.
Set it up#
Create an API key under API Keys & Devices in the dashboard (see Authentication), then tell the SDK which key to use. Do this once, before the first call:
The SDK sends requests to https://ankinize.com unless you say otherwise, so baseUrl is optional. Set it only if you run your own server, for example baseUrl: "https://anki.example.com".
Make calls#
Each function takes one object: the path values, query values and body fields side by side, with no wrapping. It returns the parsed answer, typed from the API's schemas, and throws if the call fails.
The function for an endpoint has the endpoint's operation id as its name, and the TypeScript tab under every endpoint in the API reference shows the call. Request and response types are exported too:
Searching#
Every list is narrowed by one search, q: listNotes, listCards, listDecks, listTags and listNoteTypes take it, and nothing else narrows them. It is Anki's search text (the syntax of the Anki browser) or a search value built with q, which writes the text and its escaping for you and is checked against the list it is sent to. The server does the searching, so there is no need to page through a collection to find a note.
A plain string is literal: it matches those characters in any case, and * and _ are only characters. For anything else pass a pattern: q.glob("dog*"), q.regex("^d.g"), q.accents("cafe") (ignoring accents) or q.fuzzy("japnese"). Combine searches with q.and, q.or and q.not; the terms are the ones of Anki's search (q.deck, q.notetype, q.is, q.flag, q.prop, q.added, q.rated, q.nid, ...), and q.raw takes search text the builders do not cover. A search value knows which lists it can be sent to: listDecks({ q: q.is("due") }) does not compile, because is:due says nothing about a deck.
fz:(q.fuzzy) is Ankinize's, not Anki's. Every word of the search must be close to a word of the note, in any order, ignoring case, accents and markup: a word of up to 4 characters must match exactly, one of 5 to 8 may have one typo, a longer one two. It answers yes or no, in id order, and never ranks.- A card matches when it satisfies the whole search. A note matches when one of its cards does, so
q.and(q.deck("German"), q.is("due"))finds notes with a due card in that deck. A note without cards is found only by a search that asks nothing of cards, such as tags and fields. - On
listDecks,listTagsandlistNoteTypesthe search filters names: plain text matches anywhere in the name, andq.regex,q.accents,q.fuzzyand the list's own term (q.deck,q.tag,q.notetype) work too. These lists are not paged. - The old
deckId,notetypeId,noteIdandtagparameters are gone; say it inq(q.did(id),q.mid(id),q.nid(id),q.tag(name)).
Paging through a list#
listNotes and listCards return a page and a nextCursor. To read every match, use streamNotes or streamCards: an async iterable that fetches the next page only when the last one is used up, until nextCursor is null. A search can come back with a short or empty page and a cursor, so it never stops at an empty page; stopping early reads no more pages.
By hand, pass nextCursor back as cursor until it is null.
Passing results through steps#
Everything is a plain function, so results can be passed through steps. pipe(value, f, g) runs a value through functions and flow(f, g) builds the function. iter.map, iter.filter, iter.take and iter.toArray are data-last steps for async iterables (arrays have their own methods), and indexBy, groupBy and nest take any iterable. q.andAlso, q.orElse and q.butNot continue a search inside a pipe.
Media files#
A file goes up as a Blob and comes down as one, so there is nothing to encode.
Many at once#
createNotes, updateNotes, deleteNotes, updateCards, deleteCards, createTags, updateTags (a rename) and deleteTags take one item or an array. They are the only way the SDK reaches the batch endpoints. They cut the array into calls the API accepts, send them one after the other, wait and retry when the server is busy or the rate limit asks to, and answer once with the entries in the order you gave them.
An entry the collection refuses is reported in results and the rest are written. { atomic: true } writes all or nothing, and needs everything to fit one call (200 notes, edits or tag names, 1000 ids or tag names). If a later call fails for good, the helper throws a PartialBatchError whose completed holds the answers of the calls that went through.
Type-safe names#
A note names its note type and deck, and its fields are an object keyed by each field's name in camelCase (Back Extra is backExtra). Out of the box those names are plain strings. To have the compiler check them, generate a declaration of your own collection and keep it in your project; run it again after you add a note type, a field or a deck:
After that createNote and createNotes reject a misspelt notetypeName or deckName, and a key in fields that the chosen note type does not have, and the terms q.deck, q.notetype and q.field (by the field's exact name, such as Back Extra) reject a name that is not there. Pass a pattern, for example q.glob("Lang*"), for a wildcard. Tags are left out unless you ask, with generateTypes({ tags: true }): a union of thousands of tags slows the compiler. Notes made by notetypeId are not checked.
Errors#
Any answer that is not a success throws an AnkinizeError, which is a normal Error with what the API said:
Retrying safely#
A write can take an idempotencyKey like any other field (the SDK sends it as the Idempotency-Key header). A retry with the same key returns the first answer instead of doing the work twice; see Retrying safely.
More than one key or server#
client.setConfig sets the defaults. A single call can override them with an extra, last object (the only one, when the call takes nothing else). The shared settings are left alone, so calls made for different people can run at the same time:
Your own fetch#
The SDK calls the global fetch when a request is sent, so a mock installed in a test or a polyfill installed late is used. To route requests somewhere else, pass your own:
For AI agents#
An agent that writes code against Ankinize should load the SDK skill first: it is this page in the form an agent follows best, with the full list of functions generated from the API itself. A coding agent can also be pointed at this page's Markdown.
Any page of these docs is also available as Markdown: add .md to its address (https://ankinize.com/docs/sdk.md), or ask for it with Accept: text/markdown.
Without the SDK#
The SDK adds nothing the API does not already do. If you would rather not add a package, this is the same call by hand: