# Ankinize: full documentation > The landing page and every documentation page of Ankinize, as Markdown. The index is at https://ankinize.com/llms.txt. --- Source: https://ankinize.com/ # Every card accounted for. > Ankinize is a sync server for Anki with a REST API, so scripts and AI agents can turn anything into flashcards. Your storage is one honest number: the Collection plus every Media File, counted in full. [Docs](https://ankinize.com/docs) · [OpenAPI](https://ankinize.com/api/v1/openapi.json) · [Create an account](https://ankinize.com/sign-up) ## Plans are storage and requests. The plans differ in two things: how much Storage Usage the gate allows, and how many REST API requests you may make. | Plan | Storage | Price | REST API | | | --- | --- | --- | --- | --- | | Free | 200 MB | $0 | 30 requests a minute, 1,000 a day | Sync with Anki clients, use the REST API within its limits and export your Collection. | | Pro | 1 GB | $5/mo | 120 requests a minute | Billed monthly only. Pro accounts are never deleted for inactivity. | | Max | 10 GB | $10/mo | 600 requests a minute | For heavy learners with years of images, audio and video. Billed monthly only. Max accounts are never deleted for inactivity. | ## Questions ### Which Anki clients can sync? Anki Desktop 2.1.50 and newer, AnkiDroid and AnkiMobile, through their custom sync server setting. Enter your server's base URL; the client adds /sync/ and /msync/ itself. ### What counts toward my storage? The Collection plus the full size of every Media File. There is no credit for deduplication. ### What happens over the quota? Anything that adds storage is refused, including a sync that would grow the Collection. Syncs that add nothing and media deletions always work. The REST API is read and delete only until you are back under. ### Does adding a Field force a Full Sync? Yes. Adding or removing a Field or Card Template is a Schema Change, and every device does one Full Sync afterwards. The REST API asks for ?acceptFullSync=true before it makes such a change. ### Can I take my data with me? Export your Collection as a .colpkg file whenever you like. Free accounts with no sync or login for 12 months are deleted after warnings 30 and 7 days before, each with an export link. ### How much server does self-hosting need? One vCPU and 256 MB of RAM. Collections live in local libSQL and Media Files in RustFS, an S3-compatible store, both started by the Compose file. ### How is the REST API limited? By plan, per account. Free allows 30 requests a minute and 1,000 a day, Pro 120 a minute and Max 600 a minute. Past the limit the API answers 429 with a Retry-After header, and nothing is changed. Syncing with Anki is not limited this way. ### What does "ankinize" mean? To ankinize something is to turn it into Anki flashcards: an article, a lecture, a chat, a codebase. Ankinize syncs your Collection with Anki and exposes it over a REST API, so a script or an AI agent can write the cards and your devices pick them up. ### Can an AI agent make flashcards in my Anki? Yes. Connect it through the MCP server, or give an agent an API key and it can create decks, notes and cards through the REST API or the TypeScript SDK, and run searches. The same cards sync to Anki Desktop, AnkiDroid and AnkiMobile. Agent skills for the API and SDK are published at /.well-known/agent-skills. ### Is Ankinize an AnkiConnect alternative? For automation, yes. AnkiConnect needs Anki Desktop running on your machine. The Ankinize REST API runs on the server, so scripts, CI jobs and browser extensions can add and read cards with no desktop app open. Ankinize is independent of Anki and AnkiWeb. ### How do I connect Claude, ChatGPT or Cursor? Add the /mcp address of this site as a custom MCP server and sign in with your Ankinize account when asked. You choose whether the app may only read or also change your cards. Tools that cannot use OAuth can send an API key instead. The MCP guide has the steps for each app. ### What can an MCP client do with my cards? Everything the REST API can: search and read decks, notes and cards, add and edit notes, answer cards in a review and read statistics. Untick write on the approval screen for read-only access. You can disconnect an app at any time under API Keys & Devices, and it stops working at once. --- Source: https://ankinize.com/docs # Build on your Anki collection Ankinize is a sync server for Anki with a REST API on top of the same collection. Use it to keep your devices in step, to add cards from your own tools, or to read your decks from an app you are writing. [Sync with AnkiPoint Anki on your computer or phone at Ankinize and keep every device in step.Read](https://ankinize.com/docs/sync)[AuthenticationPasswords, passkeys, two-factor and API keys, and which one to use where.Read](https://ankinize.com/docs/authentication)[API referenceRead and change decks, notes, note types and cards over HTTP, with runnable examples.Read](https://ankinize.com/docs/api)[TypeScript SDKCall the API from TypeScript with typed functions, no setup beyond a key.Read](https://ankinize.com/docs/sdk) ## How it fits together Every account has one **Collection**: your decks, notes, cards and scheduling, plus the **Media Files** (images and audio) that go with them. There are two ways to reach it, and both work on the same data. | | Anki sync | REST API | | --- | --- | --- | | Used by | Anki on desktop, Android and iOS | Your scripts, apps and automations | | Speaks | Anki's own sync protocol | JSON over HTTPS | | Signs in with | Email and password, or a sync API key | An API key (Bearer) | | Address | `https://ankinize.com/` | `https://ankinize.com/api/v1` | A note you add through the API appears in Anki on its next sync, and a note you add in Anki is there when you read it through the API. While Anki is syncing, the API waits its turn: a write during a sync is answered `423 Locked` with a `Retry-After` header, and retrying is always safe. ## Quickstart 1. Create an account and verify your email Sign up, then follow the link in the verification email. Syncing and API keys stay switched off until the address is verified. 2. Connect Anki In Anki, open *Preferences → Syncing* and set the self-hosted sync server to: Sync server ``` https://ankinize.com/ ``` Then press Sync and log in with your email and password. The [sync guide](https://ankinize.com/docs/sync) has the steps for AnkiDroid and AnkiMobile too. 3. Create an API key (optional) Under *API Keys & Devices* in the dashboard, create a key with the `read` scope. Copy it when it is shown: it is never shown again. 4. Make your first request ``` # Keep the key in an environment variable, not in your code. export ANKINIZE_API_KEY="paste-your-key-here" curl https://ankinize.com/api/v1/decks \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` You should get back the decks in your collection: Response · 200 ``` { "decks": [ { "id": 1, "name": "Default" }, { "id": 1736899200123, "name": "Languages::German" } ] } ``` > Good to know > > Your storage counts the collection plus every media file, in full. You can see how much you use on the dashboard, and export a copy of everything at any time. ## For AI agents Ankinize publishes what an agent needs to use it without a person explaining the API. There is one skill for the TypeScript SDK and one for the REST API, so an agent can pick the one that fits the code it is writing. | Resource | What it is | | --- | --- | | [https://ankinize.com/.well-known/agent-skills/ankinize-sdk/SKILL.md](https://ankinize.com/.well-known/agent-skills/ankinize-sdk/SKILL.md) | The agent skill for the TypeScript SDK: set-up, every function, batches, errors, retries and type-safe names. Load this when the task is TypeScript or JavaScript. | | [https://ankinize.com/.well-known/agent-skills/ankinize-api/SKILL.md](https://ankinize.com/.well-known/agent-skills/ankinize-api/SKILL.md) | The agent skill for the REST API: authentication, conventions and every endpoint. Use it from any other language or with plain HTTP. | | [https://ankinize.com/.well-known/agent-skills/index.json](https://ankinize.com/.well-known/agent-skills/index.json) | The discovery index of both skills, with the SHA-256 of each file. | | [https://ankinize.com/llms.txt](https://ankinize.com/llms.txt) | The llms.txt index: what Ankinize is, and a link to every page and skill. | | [https://ankinize.com/llms-full.txt](https://ankinize.com/llms-full.txt) | Every documentation page in one Markdown file. | | [https://ankinize.com/api/v1/openapi.json](https://ankinize.com/api/v1/openapi.json) | The OpenAPI document: exact parameters and responses. SDK functions are named after its operation ids. | 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`. ## Words used in these docs | Term | Meaning | | --- | --- | | Collection | Everything in your Anki profile that is not a media file: decks, notes, cards, note types and their scheduling. | | Media Files | Images, audio and video that your notes refer to. | | Note | One piece of content with named fields. Anki turns it into one or more cards. | | Note type | The template for notes: which fields they have and which cards they make. | | Full sync | Anki downloads or uploads the whole collection instead of only the changes. Some changes, such as removing a field, force one on every device. | | Scope | What an API key may do: sync, read or write. | --- Source: https://ankinize.com/docs/sync # Sync with Anki Ankinize speaks Anki's own sync protocol, so the Anki you already have works as is: you only change one setting, the address it syncs with. ## Before you start - An Ankinize account with a verified email address. - Your sync address, which is the address of this server, ending in a slash: Sync address ``` https://ankinize.com/ ``` You can copy it from *API Keys & Devices* in the dashboard. Keep the slash on the end: Anki adds `sync/` and `msync/` after it, and a server behind a reverse proxy on a sub-path needs it. ## Anki on your computer 1. Open the syncing settings Choose *Tools → Preferences → Syncing*. 2. Enter the address Paste your sync address into *Self-hosted sync server* and press OK. If Anki asks you to restart, restart it. 3. Log in and sync Press *Sync* in the main window. Log in with your Ankinize email and password. If you turned on two-factor, use an API key as the password instead; see [below](#two-factor). ## Your first sync If your computer and the server hold different collections, Anki cannot merge them and asks which side to keep. This is a **full sync**: | Choice | What happens | | --- | --- | | Upload to Ankinize | The server's copy is replaced by the one on this computer. Choose this for your first sync from your main computer. | | Download from Ankinize | The copy on this computer is replaced by the server's. Choose this on a second device. | > Check before you choose > > Both choices overwrite one side completely. If you are not sure which side has your latest reviews, export the other one first. After that, syncs only send what changed, and media files follow automatically. ## AnkiDroid 1. Open the setting Choose *Settings → Sync → Custom sync server*. 2. Enter the address AnkiDroid 2.16 and newer asks for one address: use your sync address. Older versions ask for two: Older AnkiDroid ``` Sync URL https://ankinize.com/sync/ Media sync URL https://ankinize.com/msync/ ``` 3. Log in Use the same email and password as on the computer, then sync. ## AnkiMobile (iPhone and iPad) 1. Open the setting Choose *Settings → Syncing → Custom sync server* and enter your sync address. 2. Log in and sync Log in on the sync screen and press Sync. > Two things iOS is strict about > > The server must use **HTTPS with TLS 1.2 or newer**; an older setup fails with error `-9836`. And if the server is on your home network, allow *Local Network* access for AnkiMobile in iOS Settings, or the connection is silently refused. ## If you use two-factor login Anki has nowhere to type a second code. Create an API key with the `sync` scope under *API Keys & Devices* and use it as the **password** in Anki, with your email as the username. The key works only for syncing unless you also give it the REST scopes. See [Authentication](https://ankinize.com/docs/authentication). ## Seeing and removing devices Each Anki that signs in appears under *Devices* in the dashboard, with its version and when it last synced. Sign a device out there and it has to log in again before it can sync. ## Troubleshooting | What you see | What to do | | --- | --- | | “Invalid username or password” | Check that the email is verified. If two-factor is on, use a sync API key as the password. | | A network or connection error | Open the sync address in a browser. It must load over HTTPS, and the address must end in a slash. | | Error -9836 on iPhone | The server's TLS version is too old. Serve it with TLS 1.2 or newer. | | Works on Wi‑Fi at home, not on iPhone | Allow Local Network access for AnkiMobile in iOS Settings. | | Works on a phone only with two addresses | That AnkiDroid is older than 2.16: enter the sync and media addresses separately. | | The API says “423 Locked” | A sync is running. Wait the number of seconds in Retry-After and try again. | --- Source: https://ankinize.com/docs/authentication # Authentication There are three ways to prove who you are, and each is for a different job: people use the dashboard, Anki uses its sync login, and programs use API keys. ## Which one to use | You are… | Use | Where | | --- | --- | --- | | A person in the dashboard | Email and password, a passkey, or GitHub; optionally two-factor | Log in page | | Anki syncing | Your email and password, or a sync API key as the password | Anki's sync dialog | | A script or an app | An API key, sent as a Bearer token | The REST API | ## API keys An API key lets a program act as you without knowing your password, and it can be limited to exactly what that program needs and revoked on its own. Create one under *API Keys & Devices* in the dashboard. Give it a name you will recognise, choose its scopes, and copy it when it is shown. **The key is shown once**; we store only a hash, so a lost key cannot be looked up, only replaced. ### Scopes | Scope | Allows | Does not allow | | --- | --- | --- | | `sync` | Anki sync: the collection and its media files. | The REST API. | | `read` | Every GET request in the REST API. | Changing anything. | | `write` | Every request in the REST API, including reads. | Anki sync, unless the key also has sync. | A key can carry several scopes. Give each program the fewest it needs: a dashboard that only displays decks wants `read`, not `write`. ## Sending the key Put the key in the `Authorization` header as a Bearer token. Keep it out of URLs, which end up in logs, and out of source code. ``` # The key comes from the environment, never from the source file. curl https://ankinize.com/api/v1/decks \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` ### When it is refused | Status | Code | Meaning | | --- | --- | --- | | 401 | `unauthorized` | The header is missing or the key is invalid, disabled, expired or revoked. | | 403 | `forbidden` | The key is real but may not do this: the scope is too small, it only has sync, the account is suspended or its email unverified, or it is a device key. | | 403 | `over_quota` | The account is over its storage quota. Reading and deleting still work. | ## Two-factor login and Anki Two-factor adds a code from an authenticator app to dashboard login. Anki's sync dialog has no place for that code, so once two-factor is on, Anki signs in with a `sync` API key as its **password**, and your email as the username. Your own password stops working for syncing, which is the point. > Backup codes > > When you turn two-factor on you get ten backup codes. Each signs you in once if you lose your phone, and you can make a fresh set at any time under [Settings → Security](https://ankinize.com/dashboard/settings/security). ## Devices When Anki signs in, Ankinize gives it its own device key, so that one device can be signed out without touching the others. Device keys appear under *Devices*, work for syncing only, and cannot be used with the REST API. ## Sensitive actions ask again Adding or removing a passkey and deleting your account need a fresh login. If you signed in a while ago you are asked for a code first (from your authenticator, when two-factor is on). A stolen browser session therefore cannot do these things on its own. ## Destructive changes through the API Removing a field or card template, or deleting a note type, rewrites the collection's schema and is refused unless *Allow destructive schema changes via API* is on under [API Keys & Devices](https://ankinize.com/dashboard/keys#rest-api). It is off by default; turning it on shows a warning first. Deleting decks and notes is not affected by it. --- Source: https://ankinize.com/docs/api # REST API Read and change your Anki collection over HTTP. Everything here edits the same collection that Anki syncs, on Anki schema 18, so a note created through the API appears in Anki on its next sync and the other way round. Every request is made as one user with an API key. While Anki is syncing that user's collection, writes wait: you get `423 Locked` with a `Retry-After` header, and nothing is lost if you retry. ## Quick start Create an API key with the `write` scope under *API Keys & Devices* in the dashboard, keep it in an environment variable, and make your first request. This lists your decks: ``` # Keep the key out of your shell history and your code. export ANKINIZE_API_KEY="paste-your-key-here" # List every deck in your collection. curl "https://ankinize.com/api/v1/decks" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` The TypeScript examples on this page use the official [TypeScript SDK](https://ankinize.com/docs/sdk), which makes the same requests. Agents can load the [SDK skill or the REST skill](https://ankinize.com/docs/sdk#agents). Then add a card. A note belongs to a deck and a note type, which you name (ignoring case; a nested deck is `Languages::German`). The note's `fields` is an object keyed by each field's name in camelCase: the field `Back Extra` is `backExtra`. Every field of a note type shows its key in [Get a note type](#getNoteType). You can send `notetypeId` and `deckId` instead. Reading a note gives its `fields` in the same shape, and editing one takes the same object with only the fields that change. ``` # Create a "Basic" note: the front and back of one card. curl -X POST "https://ankinize.com/api/v1/notes" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "notetypeName": "Basic", "deckName": "Languages::German", "fields": { "front": "der Hund", "back": "the dog" }, "tags": ["german"] }' ``` The card is in your collection straight away and reaches Anki on its next sync. ## Conventions ### Base URL and format Every endpoint lives under `https://ankinize.com/api/v1`. Requests and responses are JSON; send `Content-Type: application/json` on anything with a body. A request body can be up to 8 MiB, and the fields of a single note up to 5 MiB together. Ids are numbers (Anki uses millisecond timestamps), so keep them as integers. ### Authentication and scopes Send your key as `Authorization: Bearer `. Each endpoint below shows the scope it needs. A `read` key can call every `GET`; a `write` key can call everything. A key that only has the `sync` scope, and the keys used by devices, cannot use this API. See [Authentication](https://ankinize.com/docs/authentication) for how to create and limit keys. ### Errors Failures use the HTTP status that fits and one JSON shape, so you can branch on `error.code` and show `error.message` to a person: 404 Not Found ``` { "error": { "code": "not_found", "message": "Deck 1736899200123 not found" } } ``` | Status | Code | What happened | | --- | --- | --- | | `400` | `invalid_input` | The request is well formed but not valid: an unknown deck, a fields key the note type does not have. Also deck_error and card_type_error, with the same meaning. | | `400` | `invalid_json` | The body is not valid JSON. | | `401` | `unauthorized` | The key is missing, wrong, disabled or expired. | | `403` | `forbidden` | The key lacks the scope, or the account is suspended or unverified. | | `403` | `over_quota` | The account is over its storage quota. Every change except a delete is refused. | | `403` | `destructive_change_disabled` | Removing a field, a card template or a note type is switched off for this account. | | `404` | `not_found` | No such deck, note, note type, card or endpoint. | | `409` | `full_sync_required` | The change forces a full sync on every device. Repeat it with acceptFullSync=true. | | `413` | `payload_too_large` | The body is over 8 MiB. | | `422` | `validation_error` | A field is missing or has the wrong type. The message names it. | | `422` | `idempotency_conflict` | The Idempotency-Key was already used with a different body. | | `423` | `locked` | Anki is syncing, or another write is running. Retry after the Retry-After seconds. | | `429` | `rate_limited` | The plan's rate limit is used up. Wait the Retry-After seconds and send the same request again. | | `500` | `internal_error` | Something went wrong on our side. The response carries a correlationId; quote it if you ask for help. | > Validation errors are 422 > > The OpenAPI document lists `400` for a body that fails validation, but the server answers `422 validation_error`. Treat both as "fix the request". ### Rate limits Every call made with an API key counts against your plan. Free allows 30 requests a minute and 1,000 a day, Pro 120 a minute, and Max 600 a minute. Minutes start on the clock, and the day ends at 00:00 UTC. Every answer carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds) for the limit you are closest to using up. Past it, the API answers `429 rate_limited` with a `Retry-After` header. Nothing was changed, so wait that long and send the same request again. A refused call does not count. Syncing with Anki is not limited this way, and a signed media link opened in a browser is not counted. ### Retrying safely Networks fail halfway. Add an `Idempotency-Key` header (any unique string, a UUID is ideal) to a `POST`, `PATCH` or `DELETE` and a retry returns the first answer instead of doing the work twice. The answer is remembered for 24 hours, and a replay carries `X-Idempotent-Replay: true`. Reusing a key with a different body is a `422`. Failures that are worth retrying for real (`403`, `423`, `429`, `5xx`) are not remembered, so retrying them runs the request again. ### Names and casing Every parameter and body field is camelCase (`notetypeId`, `deckName`, `nextCursor`), and so are the keys of a note's `fields`. A field's key is its name cut into words at spaces, punctuation and lower-to-upper changes, the first word in lower case and the others capitalised: `The field` is `theField`, `ab_x` is `abX`, `URLText` is `urlText`, and `Question 1` is `question1`. A name with no letter or digit is its own key, and when two fields would get the same key they both keep their exact names. The exact name, in any case, always works as a key too, and a key the note type does not have is a `400` that lists the valid ones. The one exception to camelCase is the HTTP header `Idempotency-Key`, which keeps the standard header name; the SDK takes it as `idempotencyKey` and sends the header for you. Tag names go in the URL path, so percent-encode what a path cannot hold (a space is `%20`, a slash `%2F`); a tag called `batch-create`, `batch-update` or `batch-delete` can only be reached through the batch endpoints. ### When Anki is syncing While one of your devices is syncing, or another change is being written, the collection is locked and the API answers `423 Locked` with a `Retry-After` header (seconds). Nothing was changed, so wait that long and send the same request again. ### Pagination `GET /notes` and `GET /cards` return pages in ascending id order. Take `nextCursor` from a response and pass it as `cursor` to get the next page; when it is `null` you have everything. `limit` is 1 to 100 and defaults to 50. A search with `q` can scan a bounded number of cards per request, so a page may be short or even empty while `nextCursor` is still set: keep following it. ``` # The first page. Then repeat with cursor=. curl "https://ankinize.com/api/v1/notes?limit=100" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" curl "https://ankinize.com/api/v1/notes?limit=100&cursor=NEXT_CURSOR" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` ### Searching Every list takes one search, `q`, and nothing else narrows it. It is the text you would type in the Anki browser; leave it out for everything. `GET /notes`, `GET /cards` and the statistics read cards and notes; `GET /decks`, `GET /tags` and `GET /notetypes` read names (see below). A search Anki would refuse is `400 invalid_input` with Anki's own explanation, and one may be at most 1000 characters and 50 terms. Due cards in a deck, not suspended ``` curl -G "https://ankinize.com/api/v1/cards" \ --data-urlencode 'q=deck:Languages is:due -is:suspended' \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` | To match | Write | Example | | --- | --- | --- | | Note text | Words; quotes group | `"ice cream" dog` | | Combine | `-`, `or`, `and`, parentheses | `dog or cat -tag:easy` | | A field | `field:text`, `*:text` for any field | `front:dog front:*og front:re:^d` | | Notes and cards | `deck:` `note:` `card:` `tag:` `preset:` `flag:` | `deck:Languages tag:vocab flag:1` | | State | `is:new\|learn\|review\|due\|suspended\|buried\|buried-manually\|buried-sibling` | `is:due -is:suspended` | | Time | `added:` `edited:` `introduced:` `rated:days[:button]` `resched:` | `added:7 rated:1:1` | | Properties | `prop:` with `ivl`, `reps`, `lapses`, `ease`, `due`, `pos`, `rated`, and FSRS `s` `d` `r` | `prop:ivl>=21 prop:lapses>3` | | Custom data | `prop:cdn:key>1`, `prop:cds:key=x`, `has-cd:key` | `has-cd:streak` | | Ids | `nid:` `cid:` `did:` (a deck, filtered or not) `mid:` (a note type) | `did:1736899200123` | | Duplicates | `dupe:notetypeId,text` | `dupe:1736899200123,dog` | | Accents, clozes, words | `nc:` ignores accents, `sc:` looks inside clozes, `w:` a whole Unicode word | `nc:cafe` | | Regular expression | `re:regex` | `re:^d.g$` | | Typos (ours) | `fz:text` | `fz:japnese` | ### Differences from desktop Anki - Regular expressions run on the RE2 engine. A `(?x)` flag, class set operations such as `[a-z&&[^aeiou]]`, backreferences and `\Q..\E` are `400 invalid_input`, and `\b` matches ASCII word characters only, so use `w:` for Unicode words. - `fz:` is Ankinize's, not Anki's. The text and the note are cut into words (letters and digits, ignoring case and accents, with HTML and cloze markup removed), and every word of the search must match a word of the note, in any order. A word of up to 4 characters must match exactly, one of 5 to 8 may differ by one typo and a longer one by two; a typo is a missing, extra or wrong character, or two neighbours swapped. `fz:japnese` finds *Japanese*, and `fz:"dgo catt"` finds a note with *dog* and *cat* only if both are close enough. It answers yes or no, in id order, never by relevance, for up to 8 words of up to 64 characters. It also works as `front:fz:text` and `tag:fz:text`. As with `tag:` and `deck:`, a field named `fz` can no longer be searched by name. ### Notes and cards A card matches when it satisfies the whole search. A note matches when *one of its cards* does, so `deck:Languages is:due` finds notes with a card that is in the deck *and* due. A search that asks nothing of the cards (`tag:`, a field, plain text) looks at the notes alone, so it also finds a note that has no cards. A search that has to look at every card or note, such as a regular expression, `prop:s` or `fz:`, stops after about 10,000 and returns what it found with a `nextCursor`; keep following it (see Pagination). ### Decks, tags and note types On `GET /decks`, `GET /tags` and `GET /notetypes` the search filters names. Plain text matches anywhere in the name, ignoring case, with `*` and `_` as wildcards; `re:`, `nc:` and `fz:` work, and so does the list's own term: `deck:` on decks (`deck:Languages` is that deck and everything below it), `tag:` on tags (likewise) and `note:` on note types (`note:Basic*` matches whole names). A colon in plain text has to be escaped (`Languages\:\:German`). Any other term, such as `is:due`, is `400 invalid_input`. ### Changes that need everyone to re-sync Adding or reordering a field or a template changes the collection's *schema*, and Anki then asks every device to download the whole collection once. So the API stops and asks first: the request fails with `409 full_sync_required` until you repeat it with `?acceptFullSync=true`. **Removing** a field, a template or a whole note type loses data, so it also needs *Allow destructive schema changes via API* to be switched on under *Settings → Developer* in the dashboard. Otherwise you get `403 destructive_change_disabled`. Deleting decks and notes is not affected by that switch. ## Endpoints | Endpoint | What it does | Scope | | --- | --- | --- | | [GET`/decks`](#listDecks) | List decks | `read` | | [POST`/decks`](#createDeck) | Create a deck | `write` | | [GET`/decks/{id}`](#getDeck) | Get a deck by ID | `read` | | [PATCH`/decks/{id}`](#renameDeck) | Rename a deck | `write` | | [DELETE`/decks/{id}`](#deleteDeck) | Delete a deck and its subdecks | `write` | | [GET`/notes`](#listNotes) | Search notes | `read` | | [POST`/notes`](#createNote) | Create a note and generate its cards | `write` | | [POST`/notes/batch-create`](#createNotesBatch) | Create many notes in one call | `write` | | [POST`/notes/batch-update`](#updateNotesBatch) | Edit many notes in one call | `write` | | [POST`/notes/batch-delete`](#deleteNotesBatch) | Delete many notes in one call | `write` | | [GET`/notes/{id}`](#getNote) | Get note by ID with cards | `read` | | [PATCH`/notes/{id}`](#updateNote) | Update note fields or tags | `write` | | [DELETE`/notes/{id}`](#deleteNote) | Delete a note and its cards | `write` | | [GET`/note-types`](#listNoteTypes) | List note types | `read` | | [POST`/note-types`](#createNoteType) | Create note type (stock or custom) | `write` | | [GET`/note-types/{id}`](#getNoteType) | Get note type details with fields and templates | `read` | | [PATCH`/note-types/{id}`](#updateNoteType) | Update note type fields, templates, or styling | `write` | | [DELETE`/note-types/{id}`](#deleteNoteType) | Delete a note type | `write` | | [GET`/queue`](#getStudyQueue) | Get the study queue of a deck | `read` | | [GET`/tags`](#listTags) | List tags | `read` | | [POST`/tags`](#createTag) | Create a tag | `write` | | [GET`/tags/{name}`](#getTag) | Get a tag by name | `read` | | [PATCH`/tags/{name}`](#renameTag) | Rename a tag | `write` | | [DELETE`/tags/{name}`](#deleteTag) | Delete a tag | `write` | | [POST`/tags/batch-create`](#createTagsBatch) | Create many tags in one call | `write` | | [POST`/tags/batch-update`](#updateTagsBatch) | Rename many tags in one call | `write` | | [POST`/tags/batch-delete`](#deleteTagsBatch) | Delete many tags in one call | `write` | | [GET`/stats`](#getStats) | Get the statistics of a search | `read` | | [GET`/media`](#listMedia) | List media files | `read` | | [GET`/media/{name}`](#getMedia) | Download a media file | `read` | | [HEAD`/media/{name}`](#headMedia) | Get a media file's headers | `read` | | [PUT`/media/{name}`](#putMedia) | Store a media file | `write` | | [DELETE`/media/{name}`](#deleteMedia) | Delete a media file | `write` | | [GET`/cards`](#listCards) | Search cards | `read` | | [POST`/cards/batch-update`](#updateCardsBatch) | Edit many cards in one call | `write` | | [POST`/cards/batch-delete`](#deleteCardsBatch) | Delete many cards in one call | `write` | | [GET`/cards/{id}`](#getCard) | Get card details by ID | `read` | | [PATCH`/cards/{id}`](#updateCard) | Edit a card | `write` | | [DELETE`/cards/{id}`](#deleteCard) | Delete a card | `write` | | [GET`/cards/{id}/render`](#renderCard) | Render a card's front and back | `read` | | [GET`/cards/{id}/scheduling`](#getCardScheduling) | Preview the four answer buttons | `read` | | [POST`/cards/{id}/answer`](#answerCard) | Answer (review) a card | `write` | ## Decks Decks group cards for study. Nest them with `::`, for example `Languages::German`. GET`/api/v1/decks`read ### List decks Returns every deck in the collection, including the built-in `Default` deck and nested decks, or only the decks whose name `q` accepts. A nested deck's name is its full path, such as `Languages::German`. Parameters **List decks: parameters** | Name | Type | Description | | --- | --- | --- | | `q` | `query · string` | Only the decks whose name this search accepts. Plain text matches anywhere in the full name (`Languages::German`), ignoring case, with `*` and `_` as wildcards. `deck:Languages` matches that deck and everything below it, `deck:Lang*` and `deck:*::German` use wildcards. `re:regex`, `nc:text` (ignore accents) and `fz:text` (typos allowed, see [Searching](https://ankinize.com/docs/api#search)) work, and `-`, `or`, `and`, quotes and parentheses combine terms. A colon in plain text has to be escaped (`Languages\:\:German`). Any other term is `400 invalid_input`. Leave it out for every deck. | Example request ``` # List decks (needs a key with the "read" scope) curl "https://ankinize.com/api/v1/decks" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` Response 200List of decks **List decks: response fields** | Name | Type | Description | | --- | --- | --- | | `decks`required | `Deck[]` | Every deck, in no particular order. | 200 response ``` { "decks": [ { "id": 1, "name": "Default" }, { "id": 1736899200123, "name": "Languages::German" } ] } ``` - 401The `Authorization` header is missing, or the key is invalid, disabled or expired. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. POST`/api/v1/decks`write ### Create a deck Creates a deck and returns it. Use `::` in the name to nest it under a parent, for example `Languages::German`. Parameters **Create a deck: parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Create a deck: request body** | Name | Type | Description | | --- | --- | --- | | `name`required | `string` | Deck name. Use `::` to nest it. | Example body ``` { "name": "Languages::German" } ``` Example request ``` # Create a deck (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/decks" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "name": "Languages::German" }' ``` Response 201Deck created **Create a deck: response fields** | Name | Type | Description | | --- | --- | --- | | `id`required | `integer` | Numeric id. Stable for the life of the deck. | | `name`required | `string` | Full path, with `::` between parent and child. | 201 response ``` { "id": 1736899200123, "name": "Languages::German" } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 401The `Authorization` header is missing, or the key is invalid, disabled or expired. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`). - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. GET`/api/v1/decks/{id}`read ### Get a deck by ID Returns one deck by its numeric id. Parameters **Get a deck by ID: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | Example request ``` # Get a deck by ID (needs a key with the "read" scope) curl "https://ankinize.com/api/v1/decks/1736899200123" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` Response 200Deck details **Get a deck by ID: response fields** | Name | Type | Description | | --- | --- | --- | | `id`required | `integer` | Numeric id. Stable for the life of the deck. | | `name`required | `string` | Full path, with `::` between parent and child. | 200 response ``` { "id": 1736899200123, "name": "Languages::German" } ``` - 404No deck, note, note type or card has that id. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. PATCH`/api/v1/decks/{id}`write ### Rename a deck Changes a deck's name. The deck keeps its id and its cards, and its subdecks are renamed with it. Parameters **Rename a deck: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Rename a deck: request body** | Name | Type | Description | | --- | --- | --- | | `name`required | `string` | The new full name. | Example body ``` { "name": "Languages::Deutsch" } ``` Example request ``` # Rename a deck (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X PATCH "https://ankinize.com/api/v1/decks/1736899200123" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "name": "Languages::Deutsch" }' ``` Response 200Deck renamed **Rename a deck: response fields** | Name | Type | Description | | --- | --- | --- | | `id`required | `integer` | Numeric id. Stable for the life of the deck. | | `name`required | `string` | Full path, with `::` between parent and child. | 200 response ``` { "id": 1736899200123, "name": "Languages::Deutsch" } ``` - 404No deck, note, note type or card has that id. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. DELETE`/api/v1/decks/{id}`write ### Delete a deck and its subdecks Deletes the deck, all of its subdecks and every card in them. Notes left with no cards are removed too. This cannot be undone. The response lists exactly which decks and cards were removed. Deleting is still allowed while the account is over its storage quota. Parameters **Delete a deck and its subdecks: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Example request ``` # Delete a deck and its subdecks (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X DELETE "https://ankinize.com/api/v1/decks/1736899200123" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" ``` Response 200Deck deleted **Delete a deck and its subdecks: response fields** | Name | Type | Description | | --- | --- | --- | | `ok`required | `boolean` | `true` when the deck was deleted. | | `deletedDeckIds`required | `integer[]` | The deck and each of its subdecks. | | `deletedCardIds`required | `integer[]` | Every card that was in those decks. | | `deletedNoteIds`required | `integer[]` | Notes that were left with no cards and were removed with them. | | `defaultDeckReset`required | `boolean` | `true` when the built-in `Default` deck was among the decks. It cannot be removed, so it was emptied and kept. | | `returnedCardIds`required | `integer[]` | Cards that were in a deleted filtered deck. They go back to the deck they came from instead of being deleted. | 200 response ``` { "ok": true, "deletedDeckIds": [ 1736899200123 ], "defaultDeckReset": false, "deletedCardIds": [ 1736899300457, 1736899300458 ], "deletedNoteIds": [ 1736899300456 ], "returnedCardIds": [] } ``` - 404No deck, note, note type or card has that id. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. ## Notes A note holds the content (its fields and tags). Anki turns each note into one or more cards using the note type's templates. GET`/api/v1/notes`read ### Search notes Returns the notes that match a search (`q`; every note when it is empty), in ascending id order, one page at a time. `q` is the search of [Searching](https://ankinize.com/docs/api#search), including `fz:` for typos: the Anki browser's notes mode. A note is found when one of its cards satisfies the whole search, so `deck:Languages is:due` needs a card that is in the deck *and* due. A search that asks nothing of the cards (`tag:`, a field, plain text) looks at the notes alone, so it also finds a note that has no cards. To read the next page, send the previous response's `nextCursor` as `cursor`. `nextCursor` is `null` on the last page. As for cards, a search that has to look at every note, such as a regular expression or a fuzzy search, stops after about 10,000 notes and returns what it found so far with a `nextCursor`, so keep following it until it is `null`. Parameters **Search notes: parameters** | Name | Type | Description | | --- | --- | --- | | `q` | `query · string` | A search, written in Anki's search syntax: the text you would type in the browser, such as `deck:Languages is:due -tag:easy`. Leave it out for everything. The terms, the differences from desktop Anki, `fz:` for typos and the 1000-character limit are in [Searching](https://ankinize.com/docs/api#search); a search Anki would refuse is `400 invalid_input`. | | `cursor` | `query · string` | The id of the last item on the previous page, which is its `nextCursor`. Leave it out for the first page. | | `limit` | `query · integer` | Page size, from 1 to 100. Defaults to 50. | Example request ``` # Search notes (needs a key with the "read" scope) curl "https://ankinize.com/api/v1/notes?q=tag%3Avocab%20front%3Adog*&limit=50" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` Response 200Paginated list of notes **Search notes: response fields** | Name | Type | Description | | --- | --- | --- | | `notes`required | `Note[]` | This page of notes, in ascending id order. | | `nextCursor`required | `string \| null` | Send this as `cursor` to get the next page. `null` on the last page. | 200 response ``` { "notes": [ { "id": 1736899300456, "guid": "k3Jx9!aQ2c", "notetypeId": 1736899100001, "mtime": 1736899300, "usn": -1, "tags": [ "vocab", "a1" ], "fields": { "front": "der Hund", "back": "the dog" } } ], "nextCursor": "1736899300456" } ``` - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. POST`/api/v1/notes`write ### Create a note and generate its cards Creates a note and generates its cards from the note type's templates. Name the note type with `notetypeName` and the deck with `deckName` (compared ignoring case; a nested deck is `Languages::German`), or send `notetypeId` and `deckId` instead. Exactly one of `notetypeName` and `notetypeId` is needed; without a deck the note goes to the `Default` deck. A name that matches nothing is `404`. `fields` is an object with one entry per field of the note type, keyed by the field's name in camelCase: `Back Extra` is `backExtra`. Each field of a note type shows its `key` in [Get a note type](#getNoteType). The field's exact name works as a key too. A field you leave out is empty; a key that names no field is `400 invalid_input` and lists the valid keys. Send a `guid` only when you import notes from somewhere else and need to keep a stable identity; otherwise one is generated. All fields together may not exceed 5 MiB. Parameters **Create a note and generate its cards: parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Create a note and generate its cards: request body** | Name | Type | Description | | --- | --- | --- | | `notetypeName` | `string` | Which note type to use, by name (ignoring case). Send this or `notetypeId`. | | `notetypeId` | `integer` | Which note type to use, by id. See [List note types](#listNoteTypes). Send this or `notetypeName`. | | `deckName` | `string` | The deck that receives the new cards, by name (ignoring case), such as `Languages::German`. Send this or `deckId`. | | `deckId` | `integer` | The deck that receives the new cards, by id. Without a deck the note goes to the `Default` deck. | | `fields`required | `object` | The field contents, keyed by each field's key: its name in camelCase (`Back Extra` is `backExtra`) or its exact name. Fields left out are empty. HTML is allowed. | | `tags` | `string[]` | Tags to put on the note. | | `guid` | `string` | Optional. Only set it to keep an identity from another system. | Example body ``` { "notetypeName": "Basic (and reversed card)", "deckName": "Languages::German", "fields": { "front": "der Hund", "back": "the dog" }, "tags": [ "vocab", "a1" ] } ``` Example request ``` # Create a note and generate its cards (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/notes" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "notetypeName": "Basic (and reversed card)", "deckName": "Languages::German", "fields": { "front": "der Hund", "back": "the dog" }, "tags": [ "vocab", "a1" ] }' ``` Response 201Note created **Create a note and generate its cards: response fields** | Name | Type | Description | | --- | --- | --- | | `id`required | `integer` | Numeric id of the new note. | | `guid`required | `string` | The note's globally unique id: the one you sent, or a generated one. | | `cards`required | `GeneratedCard[]` | The cards the note type's templates produced. | | `newTags`required | `string[]` | Tags that did not exist in the collection before and were created by this request. | 201 response ``` { "id": 1736899300456, "guid": "k3Jx9!aQ2c", "cards": [ { "id": 1736899300457, "ord": 0, "deckId": 1736899200123, "due": 1 }, { "id": 1736899300458, "ord": 1, "deckId": 1736899200123, "due": 2 } ] } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`). - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. POST`/api/v1/notes/batch-create`write ### Create many notes in one call Creates up to 200 notes in one call. It does what [Create a note](#createNote) does for each entry (also naming note types and decks by name), but in one transaction and with the note types, decks and tags looked up once, so importing is far faster than one request per note. The answer is `200` with one entry in `results` per note you sent, in the same order (`index`). An entry the collection refuses, such as an unknown deck or a key that names no field, is reported as `ok: false` with an `error`, and the others are still written. Send `atomic: true` to write all or nothing: if any entry is refused, nothing is written and the answer is `422` with the same body. A batch counts as one request per 10 notes (at least one) against your plan's rate limit. At most 2 batches run at once on the server; when it is busy the answer is `503` with `Retry-After`, and nothing was changed. Parameters **Create many notes in one call: parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Create many notes in one call: request body** | Name | Type | Description | | --- | --- | --- | | `notes`required | `CreateNoteInput[]` | The notes to create, 1 to 200, each shaped like the body of [Create a note](#createNote). An entry that is not valid is reported in its slot. | | `atomic` | `boolean` | Write all or nothing: one refused entry means nothing is written (`422`). | Example body ``` { "notes": [ { "notetypeName": "Basic (and reversed card)", "deckName": "Languages::German", "fields": { "front": "der Hund", "back": "the dog" }, "tags": [ "vocab", "a1" ] }, { "notetypeName": "Basic (and reversed card)", "deckName": "Languages::German", "fields": { "front": "die Katze", "back": "the cat" } } ] } ``` Example request ``` # Create many notes in one call (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/notes/batch-create" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "notes": [ { "notetypeName": "Basic (and reversed card)", "deckName": "Languages::German", "fields": { "front": "der Hund", "back": "the dog" }, "tags": [ "vocab", "a1" ] }, { "notetypeName": "Basic (and reversed card)", "deckName": "Languages::German", "fields": { "front": "die Katze", "back": "the cat" } } ] }' ``` Response 200Every entry was handled; see \`results\` **Create many notes in one call: response fields** | Name | Type | Description | | --- | --- | --- | | `atomic`required | `boolean` | | | `committed`required | `boolean` | | | `created`required | `integer` | | | `failed`required | `integer` | | | `results`required | `CreateNotesBatchItem[]` | | 200 response ``` { "atomic": false, "committed": true, "created": 2, "failed": 0, "results": [ { "index": 0, "ok": true, "id": 1736899300001, "guid": "k3f9&aZ1xq", "cards": [ { "id": 1736899300002, "ord": 0, "deckId": 1736899200123, "due": 1 } ], "newTags": [ "vocab", "a1" ] }, { "index": 1, "ok": true, "id": 1736899300003, "guid": "p0L!m4Qe2c", "cards": [ { "id": 1736899300004, "ord": 0, "deckId": 1736899200123, "due": 2 } ], "newTags": [] } ] } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 422The body failed validation (`validation_error`), or the call was `atomic` and an entry was refused: then the body is the usual batch answer with every entry's `error`, and nothing was written. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. - 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed. POST`/api/v1/notes/batch-update`write ### Edit many notes in one call Edits up to 200 notes in one call. It does what [Edit a note](#updateNote) does for each entry, but in one transaction with note types, decks and tags looked up once. Each entry names a note by `id` and may send `fields`, `tags`, or the tag edits `addTags` and `removeTags`; what it leaves out stays as it was. `tags` replaces all tags and can not be sent with `addTags` or `removeTags`. `removeTags` works as in Anki: a tag is matched ignoring case, and removing `lang` also removes `lang::de` and the other tags below it (`*` is not a wildcard). A note may appear once per call. The answer is `200` with one entry in `results` per entry you sent, in the same order (`index`). An entry the collection refuses, such as an unknown note or the wrong number of fields, is reported as `ok: false` with an `error`, and the others are still written. Send `atomic: true` to write all or nothing: if any entry is refused, nothing is written and the answer is `422` with the same body. `changed` counts notes that differed from what was stored. A batch counts as one request per 10 entries (at least one) against your plan's rate limit. Parameters **Edit many notes in one call: parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Edit many notes in one call: request body** | Name | Type | Description | | --- | --- | --- | | `updates`required | `UpdateNotesBatchEntry[]` | The edits, 1 to 200. An entry that is not valid is reported in its slot. | | `atomic` | `boolean` | Write all or nothing: one refused entry means nothing is written (`422`). | Example body ``` { "updates": [ { "id": 1736899300001, "fields": { "back": "the hound" } }, { "id": 1736899300003, "addTags": [ "animals" ], "removeTags": [ "a1" ] } ] } ``` Example request ``` # Edit many notes in one call (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/notes/batch-update" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "updates": [ { "id": 1736899300001, "fields": { "back": "the hound" } }, { "id": 1736899300003, "addTags": [ "animals" ], "removeTags": [ "a1" ] } ] }' ``` Response 200Every entry was handled; see \`results\` **Edit many notes in one call: response fields** | Name | Type | Description | | --- | --- | --- | | `atomic`required | `boolean` | | | `committed`required | `boolean` | | | `changed`required | `integer` | | | `failed`required | `integer` | | | `results`required | `UpdateNotesBatchItem[]` | | 200 response ``` { "atomic": false, "committed": true, "changed": 2, "failed": 0, "results": [ { "index": 0, "ok": true, "id": 1736899300001, "changed": true, "newCards": [], "newTags": [] }, { "index": 1, "ok": true, "id": 1736899300003, "changed": true, "newCards": [], "newTags": [ "animals" ] } ] } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 422The body failed validation (`validation_error`), or the call was `atomic` and an entry was refused: then the body is the usual batch answer with every entry's `error`, and nothing was written. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. - 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed. POST`/api/v1/notes/batch-delete`write ### Delete many notes in one call Deletes up to 1000 notes and every card made from them, in one transaction. Ids that do not exist are not an error; they are listed in `missingNoteIds`. This cannot be undone. Deleting is still allowed while the account is over its storage quota. A batch counts as one request per 10 ids (at least one) against your plan's rate limit. Parameters **Delete many notes in one call: parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Delete many notes in one call: request body** | Name | Type | Description | | --- | --- | --- | | `ids`required | `integer[]` | The notes to delete, 1 to 1000. | Example body ``` { "ids": [ 1736899300001, 1736899300003 ] } ``` Example request ``` # Delete many notes in one call (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/notes/batch-delete" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "ids": [ 1736899300001, 1736899300003 ] }' ``` Response 200The notes that existed were deleted **Delete many notes in one call: response fields** | Name | Type | Description | | --- | --- | --- | | `deleted`required | `integer` | How many notes were deleted. | | `deletedNoteIds`required | `integer[]` | The notes that were deleted. | | `deletedCardIds`required | `integer[]` | Every card that was made from them. | | `missingNoteIds`required | `integer[]` | Ids that did not exist and were ignored. | 200 response ``` { "deleted": 2, "deletedNoteIds": [ 1736899300001, 1736899300003 ], "deletedCardIds": [ 1736899300002, 1736899300004 ], "missingNoteIds": [] } ``` - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`). - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. - 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed. GET`/api/v1/notes/{id}`read ### Get note by ID with cards Returns one note, including the cards it produced. Parameters **Get note by ID with cards: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | Example request ``` # Get note by ID with cards (needs a key with the "read" scope) curl "https://ankinize.com/api/v1/notes/1736899300456" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` Response 200Note details **Get note by ID with cards: response fields** | Name | Type | Description | | --- | --- | --- | | `id`required | `integer` | Numeric id. Stable for the life of the note. | | `guid`required | `string` | Globally unique id. Anki uses it to tell whether an imported note is the same one. | | `notetypeId`required | `integer` | The note type this note was made with. | | `mtime` | `integer` | When the note last changed, in seconds since 1970. | | `usn` | `integer` | Update sequence number used by sync. `-1` means changed here and not yet synced. | | `tags`required | `string[]` | Tags on the note. | | `fields`required | `object` | Every field's content, keyed by the field's key (its name in camelCase, such as `backExtra`), in the order of the note type's fields. May contain HTML. | | `cards` | `GeneratedCard[]` | | 200 response ``` { "id": 1736899300456, "guid": "k3Jx9!aQ2c", "notetypeId": 1736899100001, "mtime": 1736899300, "usn": -1, "tags": [ "vocab", "a1" ], "fields": { "front": "der Hund", "back": "the dog" }, "cards": [ { "id": 1736899300457, "ord": 0, "deckId": 1736899200123, "due": 1 } ] } ``` - 404No deck, note, note type or card has that id. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. PATCH`/api/v1/notes/{id}`write ### Update note fields or tags Changes a note's fields, its tags, or both. Anything you leave out stays as it was, including each field missing from `fields`: send only the fields that change. If the new field content makes a template produce a card it did not before, the new cards are returned in `newCards`. `changed` is `false` when the request matched what was already stored, and nothing is written. Parameters **Update note fields or tags: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Update note fields or tags: request body** | Name | Type | Description | | --- | --- | --- | | `fields` | `object` | The fields to change, keyed by each field's key (its name in camelCase, such as `backExtra`) or its exact name. Fields left out keep their content. HTML is allowed. | | `tags` | `string[]` | The complete list of tags; it replaces the existing tags. | Example body ``` { "fields": { "back": "the dog (animal)" }, "tags": [ "vocab", "a1", "animals" ] } ``` Example request ``` # Update note fields or tags (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X PATCH "https://ankinize.com/api/v1/notes/1736899300456" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "fields": { "back": "the dog (animal)" }, "tags": [ "vocab", "a1", "animals" ] }' ``` Response 200Note updated **Update note fields or tags: response fields** | Name | Type | Description | | --- | --- | --- | | `id`required | `integer` | Numeric id of the note. | | `changed`required | `boolean` | `false` when nothing differed from what was stored, so nothing was written. | | `newCards`required | `GeneratedCard[]` | Cards that the new field content caused to be generated. | | `newTags`required | `string[]` | Tags that did not exist in the collection before and were created by this request. | 200 response ``` { "id": 1736899300456, "changed": true, "newCards": [], "newTags": [ "animals" ] } ``` - 404No deck, note, note type or card has that id. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. DELETE`/api/v1/notes/{id}`write ### Delete a note and its cards Deletes a note and every card made from it. This cannot be undone. Deleting is still allowed while the account is over its storage quota. Parameters **Delete a note and its cards: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Example request ``` # Delete a note and its cards (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X DELETE "https://ankinize.com/api/v1/notes/1736899300456" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" ``` Response 200Note deleted **Delete a note and its cards: response fields** | Name | Type | Description | | --- | --- | --- | | `ok`required | `boolean` | `true` when the note was deleted. | | `deletedNoteIds`required | `integer[]` | The note that was deleted. | | `deletedCardIds`required | `integer[]` | Every card that was made from it. | 200 response ``` { "ok": true, "deletedNoteIds": [ 1736899300456 ], "deletedCardIds": [ 1736899300457, 1736899300458 ] } ``` - 404No deck, note, note type or card has that id. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. ## Note types A note type is the template for notes: its fields, and the card templates that decide which cards a note produces. GET`/api/v1/note-types`read ### List note types Returns the id and name of every note type, or only the ones whose name `q` accepts. Use [Get a note type](#getNoteType) for its fields and templates. Parameters **List note types: parameters** | Name | Type | Description | | --- | --- | --- | | `q` | `query · string` | Only the note types whose name this search accepts. Plain text matches anywhere in the name, ignoring case, with `*` and `_` as wildcards. `note:Basic*` matches whole names (the way it does in a card search), and `re:regex`, `nc:text` and `fz:text` work. `-`, `or`, `and`, quotes and parentheses combine terms. Any other term is `400 invalid_input`. Leave it out for every note type. | Example request ``` # List note types (needs a key with the "read" scope) curl "https://ankinize.com/api/v1/note-types" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` Response 200List of note types **List note types: response fields** | Name | Type | Description | | --- | --- | --- | | `noteTypes`required | `object[]` | Every note type, with its id and name. Fetch one by id for its fields and templates. | 200 response ``` { "noteTypes": [ { "id": 1736899100001, "name": "Basic" }, { "id": 1736899100002, "name": "Cloze" } ] } ``` - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. POST`/api/v1/note-types`write ### Create note type (stock or custom) Creates a note type, either from one of Anki's stock designs or from your own fields and templates. Send `stock` (`basic`, `basic-and-reversed`, `basic-optional-reversed`, `basic-type-in-answer`, `cloze` or `image-occlusion`) to start from a stock design, and optionally `name` to rename it. Or send `fields` and `templates` to define it yourself. Parameters **Create note type (stock or custom): parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Create note type (stock or custom): request body** | Name | Type | Description | | --- | --- | --- | | `stock` | `string` | Start from one of Anki's stock designs. | | `name` | `string` | Name of the note type. | | `kind` | `"normal" \| "cloze"` | | | `fields` | `object[]` | Field definitions, in order. Each needs a `name`. | | `templates` | `object[]` | Card templates. Each needs a `name`, a question format `qFormat` and an answer format `aFormat`. | | `css` | `string` | | | `sortFieldIdx` | `integer` | | Example body ``` { "stock": "basic-and-reversed", "name": "Vocabulary" } ``` Example request ``` # Create note type (stock or custom) (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/note-types" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "stock": "basic-and-reversed", "name": "Vocabulary" }' ``` Response 201Note type created **Create note type (stock or custom): response fields** | Name | Type | Description | | --- | --- | --- | | `id`required | `integer` | Numeric id of the new note type. | | `name`required | `string` | Its name. | 201 response ``` { "id": 1736899400001, "name": "Vocabulary" } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. GET`/api/v1/note-types/{id}`read ### Get note type details with fields and templates Returns a note type with its fields and card templates, its CSS and its sort field. Parameters **Get note type details with fields and templates: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | Example request ``` # Get note type details with fields and templates (needs a key with the "read" scope) curl "https://ankinize.com/api/v1/note-types/1736899100001" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` Response 200Note type details **Get note type details with fields and templates: response fields** | Name | Type | Description | | --- | --- | --- | | `id`required | `integer` | | | `name`required | `string` | | | `kind`required | `"normal" \| "cloze"` | `normal` makes one card per template; `cloze` makes one per cloze deletion. | | `css` | `string` | Stylesheet shared by every card of this note type. | | `sortFieldIdx` | `integer` | Index of the field the browser sorts by. | | `latexPre` | `string` | | | `latexPost` | `string` | | | `latexSvg` | `boolean` | | | `fields`required | `NoteTypeField[]` | | | `templates`required | `object[]` | | 200 response ``` { "id": 1736899100001, "name": "Basic", "kind": "normal", "css": ".card { font-family: arial; font-size: 20px; text-align: center; }", "sortFieldIdx": 0, "fields": [ { "ord": 0, "name": "Front", "key": "front" }, { "ord": 1, "name": "Back", "key": "back" } ], "templates": [ { "ord": 0, "name": "Card 1", "qFormat": "{{Front}}", "aFormat": "{{FrontSide}}
{{Back}}" } ] } ``` - 404No deck, note, note type or card has that id. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. PATCH`/api/v1/note-types/{id}`write ### Update note type fields, templates, or styling Changes a note type's name, CSS, sort field, fields or templates. Adding, reordering or renaming fields and templates is safe. Removing a field or template, or changing which fields a template needs, changes the collection's schema, so every device has to download the whole collection again on its next sync (a *full sync*). The request is refused with `409 full_sync_required` until you add `?acceptFullSync=true`. Removing a field or template also needs *Allow destructive schema changes via API* turned on under Settings → Developer, or the answer is `403 destructive_change_disabled`. Parameters **Update note type fields, templates, or styling: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | | `acceptFullSync` | `query · boolean` | Set to `true` to accept that this change forces every device to download the whole collection again on its next sync. Without it the request is refused with `409 full_sync_required`. | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Update note type fields, templates, or styling: request body** | Name | Type | Description | | --- | --- | --- | | `name` | `string` | | | `css` | `string` | | | `sortFieldIdx` | `integer` | | | `fields` | `object[]` | The full list of fields. A field with an `ord` is an existing one being kept or renamed; one without is new; an existing one that is missing is removed. | | `templates` | `object[]` | The full list of templates, following the same rule as `fields`. | Example body ``` { "css": ".card { font-size: 24px; }" } ``` Example request ``` # Update note type fields, templates, or styling (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X PATCH "https://ankinize.com/api/v1/note-types/1736899100001?acceptFullSync=true" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "css": ".card { font-size: 24px; }" }' ``` Response 200Note type updated **Update note type fields, templates, or styling: response fields** | Name | Type | Description | | --- | --- | --- | | `id`required | `integer` | Numeric id of the note type. | | `name`required | `string` | Its name after the change. | | `schemaChanged`required | `boolean` | `true` when the change altered the collection's schema, so every device downloads the whole collection on its next sync. | | `change`required | `NotetypeChange` | What the change did to the note type's structure: how many fields and templates were added, removed or moved. | | `updatedNotes`required | `integer` | Notes that were rewritten because a field was added, removed or moved, or the sort field changed. | | `deletedCards`required | `integer` | Cards that were deleted because their template was removed. | | `movedCards`required | `integer` | Cards whose template moved to a different position, so their number changed. | | `newCards`required | `integer` | Cards that were generated because a template was added or now produces a card. | 200 response ``` { "id": 1736899100001, "name": "Basic", "schemaChanged": false, "change": { "fieldsAdded": 0, "fieldsRemoved": 0, "fieldsMoved": 0, "sortFieldChanged": false, "templatesAdded": 0, "templatesRemoved": 0, "templatesMoved": 0, "destructive": false }, "updatedNotes": 0, "deletedCards": 0, "movedCards": 0, "newCards": 0 } ``` - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 404No deck, note, note type or card has that id. - 409The change alters the collection's schema and needs `?acceptFullSync=true` (`full_sync_required`), or the card is in a state that does not allow this (`card_state_error`: suspended, buried, in a filtered deck, or an unsupported scheduler). - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. DELETE`/api/v1/note-types/{id}`write ### Delete a note type Deletes a note type and every note that uses it. This changes the collection's schema, so it needs `?acceptFullSync=true` (otherwise `409 full_sync_required`) and *Allow destructive schema changes via API* turned on under Settings → Developer (otherwise `403 destructive_change_disabled`). Parameters **Delete a note type: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | | `acceptFullSync` | `query · boolean` | Set to `true` to accept that this change forces every device to download the whole collection again on its next sync. Without it the request is refused with `409 full_sync_required`. | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Example request ``` # Delete a note type (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X DELETE "https://ankinize.com/api/v1/note-types/1736899100001?acceptFullSync=true" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" ``` Response 200Note type deleted **Delete a note type: response fields** | Name | Type | Description | | --- | --- | --- | | `ok`required | `boolean` | `true` when the note type was deleted. | | `deletedNotes`required | `integer` | Notes that used the note type and were removed. | | `deletedCards`required | `integer` | Cards of those notes, removed with them. | | `currentNotetypeId`required | `integer` | The note type Anki now treats as current: the first by name, or a new Basic one if none were left. | | `addedBasic`required | `boolean` | `true` when that was the last note type, so a stock Basic one was added. | 200 response ``` { "ok": true, "deletedNotes": 42, "deletedCards": 42, "currentNotetypeId": 1736899100002, "addedBasic": false } ``` - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 404No deck, note, note type or card has that id. - 409The change alters the collection's schema and needs `?acceptFullSync=true` (`full_sync_required`), or the card is in a state that does not allow this (`card_state_error`: suspended, buried, in a filtered deck, or an unsupported scheduler). - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. ## Tags The labels notes carry. A tag is created when a note brings it, or here with no note yet. List them, rename a tag (its nested tags follow) or delete it from every note. Nested tags keep Anki's `::` separator; a tag name is one path segment, so percent-encode it (`Languages%3A%3AGerman`). The names `batch-create`, `batch-update` and `batch-delete` belong to the batch endpoints: a tag with such a name can only be reached through them. GET`/api/v1/tags`read ### List tags Returns every tag in the collection, sorted by name, or only the tags `q` accepts. A nested tag keeps Anki's `::` separator, such as `Languages::German`; searching `tag:Languages` also finds the notes tagged with the nested ones. Parameters **List tags: parameters** | Name | Type | Description | | --- | --- | --- | | `q` | `query · string` | Only the tags this search accepts. Plain text matches anywhere in the tag, ignoring case, with `*` and `_` as wildcards. `tag:Languages` matches that tag and the tags below it (`Languages::German`); `tag:re:regex`, `tag:nc:text` and `tag:fz:text` work as in a card search, and `re:`, `nc:` and `fz:` without `tag:` look at the whole name. `-`, `or`, `and`, quotes and parentheses combine terms. Any other term is `400 invalid_input`. Leave it out for every tag. | Example request ``` # List tags (needs a key with the "read" scope) curl "https://ankinize.com/api/v1/tags" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` Response 200List of tags **List tags: response fields** | Name | Type | Description | | --- | --- | --- | | `tags`required | `string[]` | Every tag, sorted by name. | 200 response ``` { "tags": [ "german", "Languages::German", "vocab" ] } ``` - 401The `Authorization` header is missing, or the key is invalid, disabled or expired. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. POST`/api/v1/tags`write ### Create a tag Creates a tag without putting it on a note, as Anki registers a tag when a note brings it. A name that is already used (ignoring case) answers `200` with `created: false` and the spelling that is stored, so sending it twice is safe. A nested name keeps the spelling of a parent that exists, as in Anki. A tag no note carries is removed by Anki's *Check Database* and *Clear Unused Tags*. Parameters **Create a tag: parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Create a tag: request body** | Name | Type | Description | | --- | --- | --- | | `name`required | `string` | The tag, with `::` for nesting. It can not contain a space or have an empty part. | Example body ``` { "name": "Languages::German" } ``` Example request ``` # Create a tag (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/tags" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "name": "Languages::German" }' ``` Response 200The tag already existed **Create a tag: response fields** | Name | Type | Description | | --- | --- | --- | | `name`required | `string` | The tag, spelled as it is stored. | | `created`required | `boolean` | `false` when the tag already existed. | 200 response ``` { "name": "Languages::German", "created": true } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`). - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. GET`/api/v1/tags/{name}`read ### Get a tag by name Returns a tag and `noteCount`, the notes that carry it or one of its nested tags (the notes `tag:name` finds). A tag exists when it is in the list, when a nested tag is, or when a note carries it. The `name` in the path is the tag, percent-encoded, and is compared ignoring case. Parameters **Get a tag by name: parameters** | Name | Type | Description | | --- | --- | --- | | `name`required | `path · string` | The tag's name, percent-encoded: `Languages%3A%3AGerman` is `Languages::German`. | Example request ``` # Get a tag by name (needs a key with the "read" scope) curl "https://ankinize.com/api/v1/tags/german" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` Response 200Tag details **Get a tag by name: response fields** | Name | Type | Description | | --- | --- | --- | | `name`required | `string` | The tag, spelled as it is stored. | | `noteCount`required | `integer` | How many notes carry this tag or a tag nested under it. | 200 response ``` { "name": "Languages::German", "noteCount": 42 } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 404No deck, note, note type or card has that id. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. PATCH`/api/v1/tags/{name}`write ### Rename a tag Renames a tag and every tag nested under it on all notes: renaming `Languages` to `Lang` makes `Languages::German` into `Lang::German`. The notes that change get a new modification time, so they sync. If the new name is already a tag, the two are merged, and a note that ends up with the tag twice keeps it once. Renaming a tag that no note carries renames it in the list. `notes` is how many notes were looked at and changed. Parameters **Rename a tag: parameters** | Name | Type | Description | | --- | --- | --- | | `name`required | `path · string` | The tag's name, percent-encoded: `Languages%3A%3AGerman` is `Languages::German`. | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Rename a tag: request body** | Name | Type | Description | | --- | --- | --- | | `newName`required | `string` | The new name. If it is already a tag, the two are merged. | Example body ``` { "newName": "Lang::German" } ``` Example request ``` # Rename a tag (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X PATCH "https://ankinize.com/api/v1/tags/german" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "newName": "Lang::German" }' ``` Response 200Tag renamed **Rename a tag: response fields** | Name | Type | Description | | --- | --- | --- | | `name`required | `string` | The tag's new name, as stored. | | `notes`required | `integer` | How many notes were changed. | 200 response ``` { "name": "Lang::German", "notes": 42 } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 404No deck, note, note type or card has that id. - 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`). - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. DELETE`/api/v1/tags/{name}`write ### Delete a tag Removes a tag and the tags nested under it from every note and from the list. The notes keep everything else. This cannot be undone. Deleting is still allowed while the account is over its storage quota. Parameters **Delete a tag: parameters** | Name | Type | Description | | --- | --- | --- | | `name`required | `path · string` | The tag's name, percent-encoded: `Languages%3A%3AGerman` is `Languages::German`. | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Example request ``` # Delete a tag (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X DELETE "https://ankinize.com/api/v1/tags/german" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" ``` Response 200Tag deleted **Delete a tag: response fields** | Name | Type | Description | | --- | --- | --- | | `ok`required | `boolean` | | | `deletedTags`required | `string[]` | The tags that were removed from the list: the one named and those nested under it. | | `notes`required | `integer` | How many notes lost a tag. | 200 response ``` { "ok": true, "deletedTags": [ "Lang::German", "Lang::German::Verbs" ], "notes": 57 } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 404No deck, note, note type or card has that id. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. POST`/api/v1/tags/batch-create`write ### Create many tags in one call Creates up to 200 tags in one call, in one transaction. It does what [Create a tag](#createTag) does for each name. The answer is `200` with one entry in `results` per name, in order; a name that is refused (such as one with a space) is `ok: false` with an `error` and the others are still created. Send `atomic: true` to write all or nothing: then the answer is `422` with the same body. A batch counts as one request per 10 names against your plan's rate limit. Parameters **Create many tags in one call: parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Create many tags in one call: request body** | Name | Type | Description | | --- | --- | --- | | `names`required | `string[]` | The tags to create, 1 to 200. | | `atomic` | `boolean` | Write all or nothing. See [Create many notes](#createNotesBatch). | Example body ``` { "names": [ "vocab", "grammar", "two words" ] } ``` Example request ``` # Create many tags in one call (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/tags/batch-create" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "names": [ "vocab", "grammar", "two words" ] }' ``` Response 200Every entry was handled; see \`results\` **Create many tags in one call: response fields** | Name | Type | Description | | --- | --- | --- | | `atomic`required | `boolean` | Echoes the request. | | `committed`required | `boolean` | Whether anything was written. | | `created`required | `integer` | How many tags were new. | | `failed`required | `integer` | How many entries were refused. | | `results`required | `CreateTagsBatchItem[]` | One entry per name sent, in order. | 200 response ``` { "atomic": false, "committed": true, "created": 2, "failed": 1, "results": [ { "index": 0, "ok": true, "name": "vocab", "created": true }, { "index": 1, "ok": true, "name": "grammar", "created": true }, { "index": 2, "name": "two words", "ok": false, "error": { "code": "invalid_input", "message": "a tag name can not contain a space" } } ] } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 422The body failed validation (`validation_error`), or the call was `atomic` and an entry was refused: then the body is the usual batch answer with every entry's `error`, and nothing was written. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. - 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed. POST`/api/v1/tags/batch-update`write ### Rename many tags in one call Renames up to 200 tags in one call, in one pass over the notes. Each entry names a tag by `name` and gives its `newName`, as [Rename a tag](#renameTag) does. Two entries may use the same `newName` (that merges the tags), but an entry that touches another one (a `name` that is, contains or lies inside another entry's `name` or `newName`) is refused with `invalid_input`, because the result would depend on the order. Entries are reported in `results` as in [Create many notes](#createNotesBatch); `atomic: true` writes all or nothing. Parameters **Rename many tags in one call: parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Rename many tags in one call: request body** | Name | Type | Description | | --- | --- | --- | | `updates`required | `UpdateTagsBatchEntry[]` | The renames, 1 to 200. | | `atomic` | `boolean` | Write all or nothing. See [Create many notes](#createNotesBatch). | Example body ``` { "updates": [ { "name": "vocab", "newName": "vocabulary" }, { "name": "grammar", "newName": "Grammar" } ] } ``` Example request ``` # Rename many tags in one call (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/tags/batch-update" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "updates": [ { "name": "vocab", "newName": "vocabulary" }, { "name": "grammar", "newName": "Grammar" } ] }' ``` Response 200Every entry was handled; see \`results\` **Rename many tags in one call: response fields** | Name | Type | Description | | --- | --- | --- | | `atomic`required | `boolean` | | | `committed`required | `boolean` | Whether anything was written. | | `renamed`required | `integer` | How many tags were renamed. | | `failed`required | `integer` | How many entries were refused. | | `results`required | `UpdateTagsBatchItem[]` | One entry per update sent, in order. | 200 response ``` { "atomic": false, "committed": true, "renamed": 2, "failed": 0, "results": [ { "index": 0, "ok": true, "name": "vocab", "newName": "vocabulary", "notes": 12 }, { "index": 1, "ok": true, "name": "grammar", "newName": "Grammar", "notes": 4 } ] } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 422The body failed validation (`validation_error`), or the call was `atomic` and an entry was refused: then the body is the usual batch answer with every entry's `error`, and nothing was written. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. - 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed. POST`/api/v1/tags/batch-delete`write ### Delete many tags in one call Deletes up to 1000 tags, with the tags nested under them, from every note and from the list, in one transaction. Names that do not exist are not an error; they are listed in `missingNames`. This cannot be undone. Deleting is still allowed while the account is over its storage quota. Parameters **Delete many tags in one call: parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Delete many tags in one call: request body** | Name | Type | Description | | --- | --- | --- | | `names`required | `string[]` | The tags to delete, 1 to 1000. | Example body ``` { "names": [ "vocabulary", "unused" ] } ``` Example request ``` # Delete many tags in one call (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/tags/batch-delete" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "names": [ "vocabulary", "unused" ] }' ``` Response 200The tags that existed were deleted **Delete many tags in one call: response fields** | Name | Type | Description | | --- | --- | --- | | `deleted`required | `integer` | How many tags were removed from the list. | | `deletedTags`required | `string[]` | The tags that were removed: those named and those nested under them. | | `notes`required | `integer` | How many notes lost a tag. | | `missingNames`required | `string[]` | Names that matched nothing and were ignored. | 200 response ``` { "deleted": 1, "deletedTags": [ "vocabulary" ], "notes": 12, "missingNames": [ "unused" ] } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. - 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed. ## Cards A card is one thing to review, made from a note. Change what a card shows by editing its note; here you can search, flag, suspend, bury, move and delete cards, render them for display, and answer them with Anki's v3 scheduler (SM-2 or FSRS, as the deck's preset says). GET`/api/v1/cards`read ### Search cards Returns the cards that match a search (`q`; every card when it is empty), in ascending id order, one page at a time. Narrow it by deck or note with `deck:`, `did:` and `nid:` in the search. Paginate with `nextCursor` as for notes, with one difference: a search that has to look at every card, such as a regular expression or `prop:s`, stops after about 10,000 cards and returns what it found so far together with a `nextCursor`. A page can therefore hold fewer than `limit` cards, or none, while `nextCursor` is not `null`; keep following it until it is. Parameters **Search cards: parameters** | Name | Type | Description | | --- | --- | --- | | `q` | `query · string` | A search, written in Anki's search syntax: the text you would type in the browser, such as `deck:Languages is:due -tag:easy`. Leave it out for everything. The terms, the differences from desktop Anki, `fz:` for typos and the 1000-character limit are in [Searching](https://ankinize.com/docs/api#search); a search Anki would refuse is `400 invalid_input`. | | `cursor` | `query · string` | The id of the last item on the previous page, which is its `nextCursor`. Leave it out for the first page. | | `limit` | `query · integer` | Page size, from 1 to 100. Defaults to 50. | Example request ``` # Search cards (needs a key with the "read" scope) curl "https://ankinize.com/api/v1/cards?q=deck%3ALanguages%3A%3AGerman%20is%3Adue&limit=50" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` Response 200Paginated list of cards **Search cards: response fields** | Name | Type | Description | | --- | --- | --- | | `cards`required | `Card[]` | This page of cards, in ascending id order. | | `nextCursor`required | `string \| null` | Send this as `cursor` to get the next page. `null` when the search is complete. While it is not `null` the page may hold fewer than `limit` cards, or none. | 200 response ``` { "cards": [ { "id": 1736899300457, "noteId": 1736899300456, "deckId": 1736899200123, "ord": 0, "mtime": 1736899300, "usn": -1, "type": 0, "queue": 0, "due": 1, "interval": 0, "factor": 0, "reps": 0, "lapses": 0, "left": 0, "originalDue": 0, "originalDeckId": 0, "flags": 0 } ], "nextCursor": null } ``` - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. POST`/api/v1/cards/batch-update`write ### Edit many cards in one call Applies one change to up to 1000 cards, in one transaction: set a flag, move them to a deck, suspend or bury them. It does what [Edit a card](#updateCard) does for each id, with the same rules for `patch`. The answer is `200` with one entry in `results` per distinct id. A card that does not exist, or that sits in a filtered deck and so can not be moved, is reported as `ok: false`, and the others are still changed. Send `atomic: true` to write all or nothing: if any card is refused, nothing is written and the answer is `422` with the same body. `changed` counts the cards that differed from what was stored. A batch counts as one request per 10 ids (at least one) against your plan's rate limit. Parameters **Edit many cards in one call: parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Edit many cards in one call: request body** | Name | Type | Description | | --- | --- | --- | | `ids`required | `integer[]` | The cards to change, 1 to 1000. Repeats are ignored. | | `patch`required | `UpdateCardInput` | The change, shaped like the body of [Edit a card](#updateCard). | | `atomic` | `boolean` | Write all or nothing: one refused card means nothing is written (`422`). | Example body ``` { "ids": [ 1736899300002, 1736899300004 ], "patch": { "flag": 2 } } ``` Example request ``` # Edit many cards in one call (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/cards/batch-update" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "ids": [ 1736899300002, 1736899300004 ], "patch": { "flag": 2 } }' ``` Response 200Every card was handled; see \`results\` **Edit many cards in one call: response fields** | Name | Type | Description | | --- | --- | --- | | `atomic`required | `boolean` | | | `committed`required | `boolean` | | | `changed`required | `integer` | | | `failed`required | `integer` | | | `results`required | `UpdateCardsBatchItem[]` | | - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 422The body failed validation (`validation_error`), or the call was `atomic` and an entry was refused: then the body is the usual batch answer with every entry's `error`, and nothing was written. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. - 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed. POST`/api/v1/cards/batch-delete`write ### Delete many cards in one call Deletes up to 1000 cards in one transaction. A note whose last card is deleted is deleted with it. Ids that do not exist are not an error; they are listed in `missingCardIds`. This cannot be undone. Deleting is still allowed while the account is over its storage quota. A batch counts as one request per 10 ids (at least one) against your plan's rate limit. Parameters **Delete many cards in one call: parameters** | Name | Type | Description | | --- | --- | --- | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Delete many cards in one call: request body** | Name | Type | Description | | --- | --- | --- | | `ids`required | `integer[]` | The cards to delete, 1 to 1000. | Example body ``` { "ids": [ 1736899300002, 1736899300004 ] } ``` Example request ``` # Delete many cards in one call (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X POST "https://ankinize.com/api/v1/cards/batch-delete" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "ids": [ 1736899300002, 1736899300004 ] }' ``` Response 200The cards that existed were deleted **Delete many cards in one call: response fields** | Name | Type | Description | | --- | --- | --- | | `deleted`required | `integer` | How many cards were deleted. | | `deletedCardIds`required | `integer[]` | The cards that were deleted. | | `deletedNoteIds`required | `integer[]` | Notes that were deleted because their last card went. | | `missingCardIds`required | `integer[]` | Ids that did not exist and were ignored. | 200 response ``` { "deleted": 2, "deletedCardIds": [ 1736899300002, 1736899300004 ], "deletedNoteIds": [ 1736899300001, 1736899300003 ], "missingCardIds": [] } ``` - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`). - 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`). - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. - 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed. GET`/api/v1/cards/{id}`read ### Get card details by ID Returns one card with its scheduling state: queue, due, interval, ease factor, repetitions and lapses. Parameters **Get card details by ID: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | Example request ``` # Get card details by ID (needs a key with the "read" scope) curl "https://ankinize.com/api/v1/cards/1736899300457" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" ``` Response 200Card details **Get card details by ID: response fields** | Name | Type | Description | | --- | --- | --- | | `id`required | `integer` | | | `noteId`required | `integer` | The note this card was made from. | | `deckId`required | `integer` | | | `ord`required | `integer` | Which template of the note type made this card (0 is the first). | | `mtime` | `integer` | | | `usn` | `integer` | | | `type` | `integer` | 0 new, 1 learning, 2 review, 3 relearning. | | `queue` | `integer` | Where the card sits in the scheduler: -3 or -2 buried, -1 suspended, 0 new, 1 learning, 2 review, 3 day-learning, 4 preview. | | `due` | `integer` | When it is due. For a review card this is a day number; for a new card, its position in the queue. | | `interval` | `integer` | Days until the next review, once it is a review card. | | `factor` | `integer` | Ease factor in permille: 2500 means 250%. | | `reps` | `integer` | How many times it has been reviewed. | | `lapses` | `integer` | How many times it was forgotten after being learnt. | | `left` | `integer` | | | `originalDue` | `integer` | | | `originalDeckId` | `integer` | | | `flags` | `integer` | Colour flag, 0 for none. | - 404No deck, note, note type or card has that id. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. PATCH`/api/v1/cards/{id}`write ### Edit a card Changes a card's flag, moves it to another deck, or suspends or buries it. Anything you leave out stays as it was. `flag` is 0 (none) to 7. `suspended` and `buried` cannot both be sent. Suspending keeps a card out of review until you send `suspended: false`; burying hides it only until the next day or until you unbury it. A suspended card is never buried. Moving a card clears its FSRS memory state; it is replayed from the review history with the new deck's parameters the next time the card is shown or answered. Cards in a filtered deck can neither be moved nor moved into one (`409 card_state_error`). `changed` is `false` when the request matched what was already stored. Parameters **Edit a card: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Request body **Edit a card: request body** | Name | Type | Description | | --- | --- | --- | | `deckId` | `integer` | | | `flag` | `integer` | | | `suspended` | `boolean` | | | `buried` | `boolean` | | Example body ``` { "flag": 2, "suspended": false } ``` Example request ``` # Edit a card (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X PATCH "https://ankinize.com/api/v1/cards/1736899300457" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \ -H "Content-Type: application/json" \ -d '{ "flag": 2, "suspended": false }' ``` Response 200Card updated **Edit a card: response fields** | Name | Type | Description | | --- | --- | --- | | `changed`required | `boolean` | `false` when nothing differed, so nothing was written. | | `card`required | `Card` | The card as stored after the change. | - 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`. - 404No deck, note, note type or card has that id. - 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`). - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. DELETE`/api/v1/cards/{id}`write ### Delete a card Deletes one card. If it was the last card of its note, the note is removed too. This cannot be undone. Deleting is still allowed while the account is over its storage quota. Parameters **Delete a card: parameters** | Name | Type | Description | | --- | --- | --- | | `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. | | `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. | Example request ``` # Delete a card (needs a key with the "write" scope) # Idempotency-Key makes a retry safe: the first answer is replayed. # A 423 means Anki is syncing: wait Retry-After seconds, then retry. curl -X DELETE "https://ankinize.com/api/v1/cards/1736899300457" \ -H "Authorization: Bearer $ANKINIZE_API_KEY" \ -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" ``` Response 200Card deleted **Delete a card: response fields** | Name | Type | Description | | --- | --- | --- | | `ok`required | `boolean` | `true` when the card was deleted. | | `deletedCardIds`required | `integer[]` | The card that was deleted. | | `deletedNoteIds`required | `integer[]` | The note, if the deleted card was its last one. | - 404No deck, note, note type or card has that id. - 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed. - 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again. GET`/api/v1/cards/{id}/render`read ### Render a card's front and back Renders the card as Anki shows it: the note's fields are put into the card template, so the question and the answer are ready to display. The answer includes the question above a divider, as in Anki. Everything is sanitized on the server before it is returned: scripts, event handlers, iframes, forms, `javascript:` links and remote styles are removed, and CSS is limited to a safe subset. The result is still *content from your collection*, so display it in a sandboxed `