Skip to content
AnkinizeOpenAPILog in
OverviewSync with AnkiAuthenticationAPI referenceTypeScript SDKAnkiConnect alternativeAI agentsMCP serverSelf-host

Tutorials

Browser extension

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, which makes the same requests. Agents can load the SDK skill or the REST skill.

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. 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 <key>. 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 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"
  }
}
StatusCodeWhat happened
400invalid_inputThe 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.
400invalid_jsonThe body is not valid JSON.
401unauthorizedThe key is missing, wrong, disabled or expired.
403forbiddenThe key lacks the scope, or the account is suspended or unverified.
403over_quotaThe account is over its storage quota. Every change except a delete is refused.
403destructive_change_disabledRemoving a field, a card template or a note type is switched off for this account.
404not_foundNo such deck, note, note type, card or endpoint.
409full_sync_requiredThe change forces a full sync on every device. Repeat it with acceptFullSync=true.
413payload_too_largeThe body is over 8 MiB.
422validation_errorA field is missing or has the wrong type. The message names it.
422idempotency_conflictThe Idempotency-Key was already used with a different body.
423lockedAnki is syncing, or another write is running. Retry after the Retry-After seconds.
429rate_limitedThe plan's rate limit is used up. Wait the Retry-After seconds and send the same request again.
500internal_errorSomething 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=<nextCursor from the last response>.
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 matchWriteExample
Note textWords; quotes group"ice cream" dog
Combine-, or, and, parenthesesdog or cat -tag:easy
A fieldfield:text, *:text for any fieldfront:dog front:*og front:re:^d
Notes and cardsdeck: note: card: tag: preset: flag:deck:Languages tag:vocab flag:1
Stateis:new|learn|review|due|suspended|buried|buried-manually|buried-siblingis:due -is:suspended
Timeadded: edited: introduced: rated:days[:button] resched:added:7 rated:1:1
Propertiesprop: with ivl, reps, lapses, ease, due, pos, rated, and FSRS s d rprop:ivl>=21 prop:lapses>3
Custom dataprop:cdn:key>1, prop:cds:key=x, has-cd:keyhas-cd:streak
Idsnid: cid: did: (a deck, filtered or not) mid: (a note type)did:1736899200123
Duplicatesdupe:notetypeId,textdupe:1736899200123,dog
Accents, clozes, wordsnc: ignores accents, sc: looks inside clozes, w: a whole Unicode wordnc:cafe
Regular expressionre:regexre:^d.g$
Typos (ours)fz:textfz: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#

EndpointWhat it doesScope
GET/decksList decksread
POST/decksCreate a deckwrite
GET/decks/{id}Get a deck by IDread
PATCH/decks/{id}Rename a deckwrite
DELETE/decks/{id}Delete a deck and its subdeckswrite
GET/notesSearch notesread
POST/notesCreate a note and generate its cardswrite
POST/notes/batch-createCreate many notes in one callwrite
POST/notes/batch-updateEdit many notes in one callwrite
POST/notes/batch-deleteDelete many notes in one callwrite
GET/notes/{id}Get note by ID with cardsread
PATCH/notes/{id}Update note fields or tagswrite
DELETE/notes/{id}Delete a note and its cardswrite
GET/note-typesList note typesread
POST/note-typesCreate note type (stock or custom)write
GET/note-types/{id}Get note type details with fields and templatesread
PATCH/note-types/{id}Update note type fields, templates, or stylingwrite
DELETE/note-types/{id}Delete a note typewrite
GET/queueGet the study queue of a deckread
GET/tagsList tagsread
POST/tagsCreate a tagwrite
GET/tags/{name}Get a tag by nameread
PATCH/tags/{name}Rename a tagwrite
DELETE/tags/{name}Delete a tagwrite
POST/tags/batch-createCreate many tags in one callwrite
POST/tags/batch-updateRename many tags in one callwrite
POST/tags/batch-deleteDelete many tags in one callwrite
GET/statsGet the statistics of a searchread
GET/mediaList media filesread
GET/media/{name}Download a media fileread
HEAD/media/{name}Get a media file's headersread
PUT/media/{name}Store a media filewrite
DELETE/media/{name}Delete a media filewrite
GET/cardsSearch cardsread
POST/cards/batch-updateEdit many cards in one callwrite
POST/cards/batch-deleteDelete many cards in one callwrite
GET/cards/{id}Get card details by IDread
PATCH/cards/{id}Edit a cardwrite
DELETE/cards/{id}Delete a cardwrite
GET/cards/{id}/renderRender a card's front and backread
GET/cards/{id}/schedulingPreview the four answer buttonsread
POST/cards/{id}/answerAnswer (review) a cardwrite

Decks#

Decks group cards for study. Nest them with ::, for example Languages::German.

GET/api/v1/decksread

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
NameTypeDescription
qquery · stringOnly 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) 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
NameTypeDescription
decksrequiredDeck[]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/deckswrite

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
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
namerequiredstringDeck 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
NameTypeDescription
idrequiredintegerNumeric id. Stable for the life of the deck.
namerequiredstringFull 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
NameTypeDescription
idrequiredpath · integerThe 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
NameTypeDescription
idrequiredintegerNumeric id. Stable for the life of the deck.
namerequiredstringFull 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
NameTypeDescription
idrequiredpath · integerThe numeric id, as returned by the list endpoint or when it was created.
Idempotency-Keyheader · stringAny 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
NameTypeDescription
namerequiredstringThe 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
NameTypeDescription
idrequiredintegerNumeric id. Stable for the life of the deck.
namerequiredstringFull 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
NameTypeDescription
idrequiredpath · integerThe numeric id, as returned by the list endpoint or when it was created.
Idempotency-Keyheader · stringAny 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
NameTypeDescription
okrequiredbooleantrue when the deck was deleted.
deletedDeckIdsrequiredinteger[]The deck and each of its subdecks.
deletedCardIdsrequiredinteger[]Every card that was in those decks.
deletedNoteIdsrequiredinteger[]Notes that were left with no cards and were removed with them.
defaultDeckResetrequiredbooleantrue when the built-in Default deck was among the decks. It cannot be removed, so it was emptied and kept.
returnedCardIdsrequiredinteger[]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/notesread

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, 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
NameTypeDescription
qquery · stringA 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; a search Anki would refuse is 400 invalid_input.
cursorquery · stringThe id of the last item on the previous page, which is its nextCursor. Leave it out for the first page.
limitquery · integerPage 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
NameTypeDescription
notesrequiredNote[]This page of notes, in ascending id order.
nextCursorrequiredstring | nullSend 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/noteswrite

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. 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
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
notetypeNamestringWhich note type to use, by name (ignoring case). Send this or notetypeId.
notetypeIdintegerWhich note type to use, by id. See List note types. Send this or notetypeName.
deckNamestringThe deck that receives the new cards, by name (ignoring case), such as Languages::German. Send this or deckId.
deckIdintegerThe deck that receives the new cards, by id. Without a deck the note goes to the Default deck.
fieldsrequiredobjectThe 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.
tagsstring[]Tags to put on the note.
guidstringOptional. 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
NameTypeDescription
idrequiredintegerNumeric id of the new note.
guidrequiredstringThe note's globally unique id: the one you sent, or a generated one.
cardsrequiredGeneratedCard[]The cards the note type's templates produced.
newTagsrequiredstring[]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-createwrite

Create many notes in one call#

Creates up to 200 notes in one call. It does what Create a note 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
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
notesrequiredCreateNoteInput[]The notes to create, 1 to 200, each shaped like the body of Create a note. An entry that is not valid is reported in its slot.
atomicbooleanWrite 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
NameTypeDescription
atomicrequiredboolean
committedrequiredboolean
createdrequiredinteger
failedrequiredinteger
resultsrequiredCreateNotesBatchItem[]
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-updatewrite

Edit many notes in one call#

Edits up to 200 notes in one call. It does what Edit a note 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
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
updatesrequiredUpdateNotesBatchEntry[]The edits, 1 to 200. An entry that is not valid is reported in its slot.
atomicbooleanWrite 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
NameTypeDescription
atomicrequiredboolean
committedrequiredboolean
changedrequiredinteger
failedrequiredinteger
resultsrequiredUpdateNotesBatchItem[]
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-deletewrite

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
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
idsrequiredinteger[]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
NameTypeDescription
deletedrequiredintegerHow many notes were deleted.
deletedNoteIdsrequiredinteger[]The notes that were deleted.
deletedCardIdsrequiredinteger[]Every card that was made from them.
missingNoteIdsrequiredinteger[]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
NameTypeDescription
idrequiredpath · integerThe 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
NameTypeDescription
idrequiredintegerNumeric id. Stable for the life of the note.
guidrequiredstringGlobally unique id. Anki uses it to tell whether an imported note is the same one.
notetypeIdrequiredintegerThe note type this note was made with.
mtimeintegerWhen the note last changed, in seconds since 1970.
usnintegerUpdate sequence number used by sync. -1 means changed here and not yet synced.
tagsrequiredstring[]Tags on the note.
fieldsrequiredobjectEvery 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.
cardsGeneratedCard[]
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
NameTypeDescription
idrequiredpath · integerThe numeric id, as returned by the list endpoint or when it was created.
Idempotency-Keyheader · stringAny 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
NameTypeDescription
fieldsobjectThe 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.
tagsstring[]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
NameTypeDescription
idrequiredintegerNumeric id of the note.
changedrequiredbooleanfalse when nothing differed from what was stored, so nothing was written.
newCardsrequiredGeneratedCard[]Cards that the new field content caused to be generated.
newTagsrequiredstring[]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
NameTypeDescription
idrequiredpath · integerThe numeric id, as returned by the list endpoint or when it was created.
Idempotency-Keyheader · stringAny 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
NameTypeDescription
okrequiredbooleantrue when the note was deleted.
deletedNoteIdsrequiredinteger[]The note that was deleted.
deletedCardIdsrequiredinteger[]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-typesread

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 for its fields and templates.

Parameters

List note types: parameters
NameTypeDescription
qquery · stringOnly 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
NameTypeDescription
noteTypesrequiredobject[]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-typeswrite

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
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
stockstringStart from one of Anki's stock designs.
namestringName of the note type.
kind"normal" | "cloze"
fieldsobject[]Field definitions, in order. Each needs a name.
templatesobject[]Card templates. Each needs a name, a question format qFormat and an answer format aFormat.
cssstring
sortFieldIdxinteger
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
NameTypeDescription
idrequiredintegerNumeric id of the new note type.
namerequiredstringIts 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
NameTypeDescription
idrequiredpath · integerThe 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
NameTypeDescription
idrequiredinteger
namerequiredstring
kindrequired"normal" | "cloze"normal makes one card per template; cloze makes one per cloze deletion.
cssstringStylesheet shared by every card of this note type.
sortFieldIdxintegerIndex of the field the browser sorts by.
latexPrestring
latexPoststring
latexSvgboolean
fieldsrequiredNoteTypeField[]
templatesrequiredobject[]
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}}<hr id=answer>{{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
NameTypeDescription
idrequiredpath · integerThe numeric id, as returned by the list endpoint or when it was created.
acceptFullSyncquery · booleanSet 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-Keyheader · stringAny 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
NameTypeDescription
namestring
cssstring
sortFieldIdxinteger
fieldsobject[]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.
templatesobject[]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
NameTypeDescription
idrequiredintegerNumeric id of the note type.
namerequiredstringIts name after the change.
schemaChangedrequiredbooleantrue when the change altered the collection's schema, so every device downloads the whole collection on its next sync.
changerequiredNotetypeChangeWhat the change did to the note type's structure: how many fields and templates were added, removed or moved.
updatedNotesrequiredintegerNotes that were rewritten because a field was added, removed or moved, or the sort field changed.
deletedCardsrequiredintegerCards that were deleted because their template was removed.
movedCardsrequiredintegerCards whose template moved to a different position, so their number changed.
newCardsrequiredintegerCards 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
NameTypeDescription
idrequiredpath · integerThe numeric id, as returned by the list endpoint or when it was created.
acceptFullSyncquery · booleanSet 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-Keyheader · stringAny 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
NameTypeDescription
okrequiredbooleantrue when the note type was deleted.
deletedNotesrequiredintegerNotes that used the note type and were removed.
deletedCardsrequiredintegerCards of those notes, removed with them.
currentNotetypeIdrequiredintegerThe note type Anki now treats as current: the first by name, or a new Basic one if none were left.
addedBasicrequiredbooleantrue 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/tagsread

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
NameTypeDescription
qquery · stringOnly 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
NameTypeDescription
tagsrequiredstring[]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/tagswrite

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
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
namerequiredstringThe 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
NameTypeDescription
namerequiredstringThe tag, spelled as it is stored.
createdrequiredbooleanfalse 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
NameTypeDescription
namerequiredpath · stringThe 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
NameTypeDescription
namerequiredstringThe tag, spelled as it is stored.
noteCountrequiredintegerHow 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
NameTypeDescription
namerequiredpath · stringThe tag's name, percent-encoded: Languages%3A%3AGerman is Languages::German.
Idempotency-Keyheader · stringAny 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
NameTypeDescription
newNamerequiredstringThe 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
NameTypeDescription
namerequiredstringThe tag's new name, as stored.
notesrequiredintegerHow 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
NameTypeDescription
namerequiredpath · stringThe tag's name, percent-encoded: Languages%3A%3AGerman is Languages::German.
Idempotency-Keyheader · stringAny 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
NameTypeDescription
okrequiredboolean
deletedTagsrequiredstring[]The tags that were removed from the list: the one named and those nested under it.
notesrequiredintegerHow 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-createwrite

Create many tags in one call#

Creates up to 200 tags in one call, in one transaction. It does what Create a tag 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
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
namesrequiredstring[]The tags to create, 1 to 200.
atomicbooleanWrite all or nothing. See Create many notes.
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
NameTypeDescription
atomicrequiredbooleanEchoes the request.
committedrequiredbooleanWhether anything was written.
createdrequiredintegerHow many tags were new.
failedrequiredintegerHow many entries were refused.
resultsrequiredCreateTagsBatchItem[]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-updatewrite

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 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; atomic: true writes all or nothing.

Parameters

Rename many tags in one call: parameters
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
updatesrequiredUpdateTagsBatchEntry[]The renames, 1 to 200.
atomicbooleanWrite all or nothing. See Create many notes.
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
NameTypeDescription
atomicrequiredboolean
committedrequiredbooleanWhether anything was written.
renamedrequiredintegerHow many tags were renamed.
failedrequiredintegerHow many entries were refused.
resultsrequiredUpdateTagsBatchItem[]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-deletewrite

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
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
namesrequiredstring[]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
NameTypeDescription
deletedrequiredintegerHow many tags were removed from the list.
deletedTagsrequiredstring[]The tags that were removed: those named and those nested under them.
notesrequiredintegerHow many notes lost a tag.
missingNamesrequiredstring[]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/cardsread

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
NameTypeDescription
qquery · stringA 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; a search Anki would refuse is 400 invalid_input.
cursorquery · stringThe id of the last item on the previous page, which is its nextCursor. Leave it out for the first page.
limitquery · integerPage 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
NameTypeDescription
cardsrequiredCard[]This page of cards, in ascending id order.
nextCursorrequiredstring | nullSend 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-updatewrite

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 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
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
idsrequiredinteger[]The cards to change, 1 to 1000. Repeats are ignored.
patchrequiredUpdateCardInputThe change, shaped like the body of Edit a card.
atomicbooleanWrite 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
NameTypeDescription
atomicrequiredboolean
committedrequiredboolean
changedrequiredinteger
failedrequiredinteger
resultsrequiredUpdateCardsBatchItem[]
  • 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-deletewrite

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
NameTypeDescription
Idempotency-Keyheader · stringAny 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
NameTypeDescription
idsrequiredinteger[]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
NameTypeDescription
deletedrequiredintegerHow many cards were deleted.
deletedCardIdsrequiredinteger[]The cards that were deleted.
deletedNoteIdsrequiredinteger[]Notes that were deleted because their last card went.
missingCardIdsrequiredinteger[]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
NameTypeDescription
idrequiredpath · integerThe 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
NameTypeDescription
idrequiredinteger
noteIdrequiredintegerThe note this card was made from.
deckIdrequiredinteger
ordrequiredintegerWhich template of the note type made this card (0 is the first).
mtimeinteger
usninteger
typeinteger0 new, 1 learning, 2 review, 3 relearning.
queueintegerWhere the card sits in the scheduler: -3 or -2 buried, -1 suspended, 0 new, 1 learning, 2 review, 3 day-learning, 4 preview.
dueintegerWhen it is due. For a review card this is a day number; for a new card, its position in the queue.
intervalintegerDays until the next review, once it is a review card.
factorintegerEase factor in permille: 2500 means 250%.
repsintegerHow many times it has been reviewed.
lapsesintegerHow many times it was forgotten after being learnt.
leftinteger
originalDueinteger
originalDeckIdinteger
flagsintegerColour 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
NameTypeDescription
idrequiredpath · integerThe numeric id, as returned by the list endpoint or when it was created.
Idempotency-Keyheader · stringAny 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
NameTypeDescription
deckIdinteger
flaginteger
suspendedboolean
buriedboolean
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
NameTypeDescription
changedrequiredbooleanfalse when nothing differed, so nothing was written.
cardrequiredCardThe 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
NameTypeDescription
idrequiredpath · integerThe numeric id, as returned by the list endpoint or when it was created.
Idempotency-Keyheader · stringAny 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
NameTypeDescription
okrequiredbooleantrue when the card was deleted.
deletedCardIdsrequiredinteger[]The card that was deleted.
deletedNoteIdsrequiredinteger[]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}/renderread

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 <iframe sandbox> or srcdoc and not straight into your page.

With format=json (the default) you get question, answer and css. With format=html you get one complete HTML document, with a Content-Security-Policy that allows no script and the sandbox directive, which is meant for <iframe sandbox srcdoc>: fetch it with your key and put the text in srcdoc (an iframe's src cannot send the key, and this server refuses to be framed). Media in a card is referred to by file name and listed in media. The file names in the question and answer are turned into signed links to GET /api/v1/media/{name} that last an hour, so the card shows its pictures and plays its audio from a sandboxed frame; or pass mediaBaseUrl (an http(s) URL ending in /) to have them point at files you host instead.

Typing the answer. For {{type:Field}}, {{type:nc:Field}} (accents ignored) and {{type:cloze:Field}} the question holds <div class="typeans" data-field="Field"></div>, where your own input goes. Pass what the learner typed as typedAnswer (up to 1000 characters) and the answer shows the comparison Anki shows, computed the same way: a <code id="typeans"> with the typed text and the expected text as typeGood, typeBad and typeMissed spans around an arrow, in the font and size of the field. The classes carry no colors, so style them in your page. Without typedAnswer the answer shows the expected text. A text too long to compare is shown as one change.

Formulas. \(...\) and \[...\] are returned as written. Anki's [$]...[/$] and [$$]...[/$$] tags are rewritten to them, and [latex]...[/latex] becomes display math; images are not generated, because that needs a TeX installation. math is true when the question or the answer holds a formula. Typeset it in your page with MathJax 3 after inserting the HTML (format=html allows no script, so a formula stays as text there). TeX that is not math, such as packages or TikZ, is not supported.

A template that cannot be rendered is 422 render_error.

Parameters

Render a card's front and back: parameters
NameTypeDescription
idrequiredpath · integerThe numeric id, as returned by the list endpoint or when it was created.
formatquery · stringjson (default) or html for one complete, sandboxed HTML document.
typedAnswerquery · stringWhat the learner typed, for a {{type:Field}} comparison. At most 1000 characters.
mediaBaseUrlquery · stringAn http(s) URL prefix. Media file names in the card are turned into this prefix plus the URL-encoded name.

Example request

# Render a card's front and back (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/cards/1736899300457/render" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"

Response

200The rendered card

Render a card's front and back: response fields
NameTypeDescription
cardIdrequiredinteger
noteIdinteger
notetypeIdinteger
deckIdinteger
ordinteger
isEmptyboolean
mathrequiredbooleanThe question or the answer holds a formula (\(...\) or \[...\]) for MathJax to typeset.
questionrequiredstring
answerrequiredstring
cssrequiredstring
mediarequiredstring[]
  • 404No deck, note, note type or card has that id.
  • 422The card's template has a syntax error or refers to a field that does not exist (render_error).
  • 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}/schedulingread

Preview the four answer buttons#

Shows what each answer button would do, without answering: the time until the card would next be due after Again, Hard, Good and Easy. This is the preview Anki shows above its buttons.

Only the v3 scheduler is supported. The card must be answerable: not suspended or buried (409 card_state_error). A card in a filtered deck is previewed as Anki does: in a deck that reschedules, the buttons are the home deck preset's, and a review that comes before it was due is scheduled from the days it was early; in a deck that does not reschedule, the buttons are the preview delays (Again, Hard and Good come back after the deck's delays, a delay of 0 ends the preview, and Easy always ends it) and show 0 seconds when they end it.

Review intervals are fuzzed and load balanced like Anki's: the day is drawn within the fuzz range, weighted towards days with fewer cards of the same preset due, the preset's Easy Days and (if it buries siblings) days away from the card's siblings. The draw is fixed by the card and its review count, so the preview, the answer and a repeat of either agree.

Parameters

Preview the four answer buttons: parameters
NameTypeDescription
idrequiredpath · integerThe numeric id, as returned by the list endpoint or when it was created.

Example request

# Preview the four answer buttons (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/cards/1736899300457/scheduling" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"

Response

200What each answer would do

Preview the four answer buttons: response fields
NameTypeDescription
cardIdrequiredinteger
fsrsrequiredboolean
todayinteger
nextDayAtinteger
buttonsrequiredobject[]
  • 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).
  • 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/{id}/answerwrite

Answer (review) a card#

Answers a card, the same as pressing a button in Anki's reviewer. The card is rescheduled with the deck preset's v3 scheduler (or FSRS when the preset turns it on), a review-log entry is written, the deck's daily counts go up, and siblings are buried if the preset says so. A card that reaches the leech threshold gets the leech tag and, if the preset says so, is suspended.

rating is again, hard, good or easy (or 1 to 4). timeTakenMs is how long the learner looked at the card, capped by the preset. Send an Idempotency-Key so a retry does not answer twice.

The card must not be suspended or buried, and the collection must use the v3 scheduler; otherwise the answer is 409 card_state_error. A card in a filtered deck is answered as Anki does. The preset, the answer-time cap and burying come from the card's home deck, the daily counts go to the filtered deck and its parents, and the review log records an answer given before the card was due (or in a deck that does not reschedule) with type 3 (filtered). In a deck that reschedules, a review card that is passed returns to its home deck, and a learning card stays in the filtered deck until it graduates; in a deck that does not reschedule, nothing is rescheduled: Again, Hard and Good bring the card back after the deck's preview delay, and the card returns to its home deck in its old state when the preview ends. The legacy schedulers are not supported. A card that has no FSRS memory state yet (for example one that was just moved into an FSRS deck) gets one replayed from its review history, as Anki does, and from its SM-2 ease and interval when the history is too short.

The load balancer is applied to review intervals as in Anki (see GET /api/v1/cards/{id}/scheduling). It reads the cards due in the next 99 days when the card is answered (Anki reads them once when it builds a queue), and the weekday used for Easy Days is taken in the collection's localOffset (UTC when unset).

Parameters

Answer (review) a card: parameters
NameTypeDescription
idrequiredpath · integerThe numeric id, as returned by the list endpoint or when it was created.
Idempotency-Keyheader · stringAny 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

Answer (review) a card: request body
NameTypeDescription
ratingrequiredobject
timeTakenMsinteger
Example body
{
  "rating": "good",
  "timeTakenMs": 4200
}

Example request

# Answer (review) 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 POST "https://ankinize.com/api/v1/cards/1736899300457/answer" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "rating": "good",
    "timeTakenMs": 4200
  }'

Response

200The card was scheduled and the review logged

Answer (review) a card: response fields
NameTypeDescription
revlogIdrequiredintegerId of the review-log entry, a millisecond timestamp.
leechedrequiredbooleantrue when this answer made the card a leech.
buriedSiblingIdsrequiredinteger[]Other cards of the same note that were buried until tomorrow.
cardrequiredCardThe card as stored after the answer.
  • 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).
  • 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.

Study#

What to study next. The study queue is built the way Anki builds it, from the deck presets, daily limits and the current time.

GET/api/v1/queueread

Get the study queue of a deck#

Returns the cards Anki would show next in a deck, in the order it would show them: the deck (and its subdecks) is searched for learning cards due now, then reviews and day-learning cards, then new cards, under the daily limits of every deck preset and deck (including New cards ignore review limit and Apply all parent limits), with new-card gather and sort order, review order, FSRS orders, burying of siblings and the mixes of new cards and day-learning cards with reviews. The result matches the desktop client's queue for the same collection and moment.

counts is what Anki shows above the buttons (new, learning, review). cards holds the first limit cards, each with a kind of new, learning or review; total is the length of the whole queue. Learning cards that are due within the preset's learn-ahead limit are listed at the end. Nothing is written, so asking again before you answer gives the same queue; after POST /api/v1/cards/{id}/answer ask again.

Without deckId the collection's current deck is used. Only the v3 scheduler is supported (409 card_state_error otherwise). A filtered deck is queued with unlimited counts, in the order it was built in Anki (its cards keep that position).

Parameters

Get the study queue of a deck: parameters
NameTypeDescription
deckIdquery · integerOnly items that belong to this deck.
limitquery · integerPage size, from 1 to 100. Defaults to 50.

Example request

# Get the study queue of a deck (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/queue?deckId=1736899200123&limit=50" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"

Response

200The next cards to study, in order

Get the study queue of a deck: response fields
NameTypeDescription
deckIdrequiredinteger
countsrequiredobject
totalrequiredinteger
learnAheadSecsrequiredinteger
cardsrequiredobject[]
  • 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).
  • 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/statsread

Get the statistics of a search#

Returns the graphs of Anki's statistics screen for the cards a search matches (q, default every card) and the last days days of review history (0, the default, is all of it). The numbers are Anki's: the same counts, buckets and averages the desktop client draws, computed from the review log and the cards in place. Nothing is copied into another database. A request reads the review log once, so a big collection takes a moment; one that takes longer than 20 seconds is refused with 400 invalid_input (narrow the search or the days). The server keeps the graphs of a search in memory for up to five minutes, and never past the end of the day, while the collection is unchanged, so a repeat request is answered without reading the collection again; any change to the collection ends that.

Days are counted from the end of the current day, so 0 is today, -1 yesterday and 1 tomorrow; a day starts at the collection's rollover hour and is read in its localOffset (UTC when unset), as timing says. buttons, hours and trueRetention count answers over a month, three months, a year and all time. cardCounts counts cards by kind, with suspended and buried cards counted separately or together. eases (SM-2) and difficulty, stability and retrievability (FSRS) cover the cards that have the data; fsrs says whether the collection has FSRS turned on. retrievability is the chance of recalling each card now, from its memory state and the time since its last review. Keys of the count objects are numbers: a day, a percent bin or an interval in days.

Parameters

Get the statistics of a search: parameters
NameTypeDescription
qquery · stringA 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; a search Anki would refuse is 400 invalid_input.
daysquery · integerDays of review history to count, from today back; 0 (the default) is all of it. At most 36500.

Example request

# Get the statistics of a search (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/stats" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"

Response

200The graphs of Anki's statistics screen

Get the statistics of a search: response fields
NameTypeDescription
buttonsrequiredobjectHow many times each answer button (Again, Hard, Good, Easy: the four numbers, in that order) was pressed, in learning, on young review cards (interval under 21 days) and on mature ones, over the last month, three months, year and all time.
cardCountsrequiredobjectCards by kind. includingInactive counts suspended and buried cards by their kind; excludingInactive counts them only as suspended and buried. young and mature are review cards with an interval under and over 21 days.
hoursrequiredobjectAnswers by hour of the day (24 entries, hour 0 first), in the collection's localOffset: how many (total) and how many were not Again (correct), over the last month, three months, year and all time.
todayrequiredobjectWhat was answered since the last day rollover: the number of answers and the milliseconds they took, how many were not Again, how many of those were on mature cards, and the answers by kind (earlyReviewCount is answers in a filtered deck).
easesrequiredobjectSM-2 cards: how many have each ease, as a percent (250 is 250%), and the median ease. Cards with an FSRS memory state are in difficulty.
difficultyrequiredobjectFSRS cards: how many fall in each 1% bin of difficulty (0 to 99), and the median difficulty in percent.
intervalsrequiredobjectReview and relearning cards by interval, in days.
stabilityrequiredobjectFSRS cards by stability, in days, rounded.
futureDuerequiredobjectCards that are due, by day (0 is today, 1 tomorrow, negative is overdue): suspended and new cards are left out, and buried cards that are already due. haveBacklog is true when something is overdue. dailyLoad is the average number of reviews a day the collection needs, from the intervals.
addedrequiredobjectCards by the day they were created (0 is today, -1 yesterday).
reviewsrequiredobjectAnswers (count) and the milliseconds they took (time) by day (0 is today, -1 yesterday), split into learning, relearning, young and mature review cards and filtered decks. Manual changes to the schedule are not answers.
rolloverHourrequiredintegerThe hour of the day a new day starts (the rollover setting).
retrievabilityrequiredobjectFSRS cards: how many fall in each 1% bin of the chance of recalling them now, the average in percent, and the sum of the chances over cards and over notes (the expected number of cards, and of notes, remembered).
fsrsrequiredbooleanWhether the collection has FSRS turned on.
trueRetentionrequiredobjectOf the review answers on cards that had an interval of a day or more, how many were passed and how many failed (Again), for young and mature cards: today, yesterday, the last week, month and year, and all time.
cardsrequiredintegerThe number of cards the search matched.
reviewEntriesrequiredintegerThe number of review-log entries read, which is the history inside days of the matched cards (all entries for an empty search, even those of deleted cards).
timingrequiredobjectThe clock the graphs use: now (when they were computed, which for a repeated request can be a few minutes ago) and nextDayAt (when the current day ends) in seconds since the epoch, daysElapsed since the collection was created, and localOffsetSecs east of UTC.
200 response
{
  "buttons": {
    "oneMonth": {
      "learning": [
        4,
        1,
        22,
        3
      ],
      "young": [
        6,
        3,
        40,
        5
      ],
      "mature": [
        2,
        1,
        31,
        4
      ]
    },
    "threeMonths": {
      "learning": [
        9,
        2,
        51,
        6
      ],
      "young": [
        12,
        5,
        88,
        9
      ],
      "mature": [
        5,
        2,
        70,
        8
      ]
    },
    "oneYear": {
      "learning": [
        9,
        2,
        51,
        6
      ],
      "young": [
        12,
        5,
        88,
        9
      ],
      "mature": [
        5,
        2,
        70,
        8
      ]
    },
    "allTime": {
      "learning": [
        9,
        2,
        51,
        6
      ],
      "young": [
        12,
        5,
        88,
        9
      ],
      "mature": [
        5,
        2,
        70,
        8
      ]
    }
  },
  "cardCounts": {
    "includingInactive": {
      "new": 120,
      "learn": 3,
      "relearn": 2,
      "young": 40,
      "mature": 85,
      "suspended": 0,
      "buried": 0
    },
    "excludingInactive": {
      "new": 118,
      "learn": 3,
      "relearn": 2,
      "young": 40,
      "mature": 84,
      "suspended": 1,
      "buried": 1
    }
  },
  "hours": {
    "oneMonth": [
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 40,
        "correct": 36
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 40,
        "correct": 36
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      },
      {
        "total": 0,
        "correct": 0
      }
    ],
    "threeMonths": [],
    "oneYear": [],
    "allTime": []
  },
  "today": {
    "answerCount": 42,
    "answerMillis": 301000,
    "correctCount": 38,
    "matureCorrect": 20,
    "matureCount": 22,
    "learnCount": 6,
    "reviewCount": 34,
    "relearnCount": 2,
    "earlyReviewCount": 0
  },
  "eases": {
    "eases": {
      "230": 4,
      "250": 60,
      "270": 9
    },
    "average": 250
  },
  "difficulty": {
    "eases": {},
    "average": 0
  },
  "intervals": {
    "intervals": {
      "1": 5,
      "7": 18,
      "30": 40
    }
  },
  "stability": {
    "intervals": {}
  },
  "futureDue": {
    "futureDue": {
      "0": 12,
      "1": 9,
      "2": 11
    },
    "haveBacklog": false,
    "dailyLoad": 14
  },
  "added": {
    "added": {
      "-30": 40,
      "-1": 6
    }
  },
  "reviews": {
    "count": {
      "-1": {
        "learn": 4,
        "relearn": 1,
        "young": 22,
        "mature": 30,
        "filtered": 0
      }
    },
    "time": {
      "-1": {
        "learn": 20000,
        "relearn": 5000,
        "young": 110000,
        "mature": 150000,
        "filtered": 0
      }
    }
  },
  "rolloverHour": 4,
  "retrievability": {
    "retrievability": {},
    "average": 0,
    "sumByCard": 0,
    "sumByNote": 0
  },
  "fsrs": false,
  "trueRetention": {
    "today": {
      "youngPassed": 20,
      "youngFailed": 2,
      "maturePassed": 18,
      "matureFailed": 2
    },
    "yesterday": {
      "youngPassed": 18,
      "youngFailed": 3,
      "maturePassed": 17,
      "matureFailed": 1
    },
    "week": {
      "youngPassed": 120,
      "youngFailed": 14,
      "maturePassed": 110,
      "matureFailed": 9
    },
    "month": {
      "youngPassed": 400,
      "youngFailed": 40,
      "maturePassed": 380,
      "matureFailed": 30
    },
    "year": {
      "youngPassed": 400,
      "youngFailed": 40,
      "maturePassed": 380,
      "matureFailed": 30
    },
    "allTime": {
      "youngPassed": 400,
      "youngFailed": 40,
      "maturePassed": 380,
      "matureFailed": 30
    }
  },
  "cards": 250,
  "reviewEntries": 1190,
  "timing": {
    "now": 1736899200,
    "daysElapsed": 412,
    "nextDayAt": 1736913600,
    "localOffsetSecs": 0
  }
}
  • 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.
  • 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.

Media#

The images, audio and video your cards refer to. They are the same files the Anki desktop and mobile clients sync: a file you put here reaches your devices on their next media sync, and a file synced from a device can be read here.

GET/api/v1/mediaread

List media files#

Lists the collection's media files by name, 100 at a time (limit up to 1000). prefix keeps only the names that start with it. When nextCursor is not null, pass it as cursor to get the next page. Each item has the file's size, its sha1 and sha256, the contentType it is served as, and the usn of its last change, which is the number the Anki media sync uses.

Parameters

List media files: parameters
NameTypeDescription
prefixquery · string
limitquery · integerPage size, from 1 to 100. Defaults to 50.
cursorquery · stringThe id of the last item on the previous page, which is its nextCursor. Leave it out for the first page.

Example request

# List media files (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/media?limit=50" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"

Response

200A page of media files, in name order

List media files: response fields
NameTypeDescription
itemsrequiredMediaFile[]
nextCursorrequiredstring | null
  • 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.
  • 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/media/{name}read

Download a media file#

Returns the file's bytes. The type comes from the file name's extension only, never from the bytes, and every response says X-Content-Type-Options: nosniff and a Content-Security-Policy with sandbox, so a file from your collection cannot run script on this origin. Images, audio, video and fonts are shown inline; SVG, PDF and any other type are offered as downloads (Content-Disposition: attachment).

The ETag is the file's SHA-256, so If-None-Match answers 304. Range: bytes=... (one range) answers 206, which lets a browser seek in audio and video; a range past the end is 416.

Instead of a key, the request may carry the u, exp and sig query parameters of a signed link. A signed link opens that one file for reading until it expires, which is how a card shown in a sandboxed frame loads its pictures: GET /api/v1/cards/{id}/render puts such links into the card unless you pass mediaBaseUrl. The links last an hour.

Parameters

Download a media file: parameters
NameTypeDescription
namerequiredpath · stringThe tag's name, percent-encoded: Languages%3A%3AGerman is Languages::German.
Rangeheader · string
If-None-Matchheader · string
uquery · string
expquery · integer
sigquery · string

Example request

# Download a media file (needs a key with the "read" scope)
curl -o cat.png "https://ankinize.com/api/v1/media/cat.png" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"

Response

200The file

  • 304The ETag still matches
  • 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.
  • 416The range is not inside the file
  • 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.
HEAD/api/v1/media/{name}read

Get a media file's headers#

Like GET, without the body: the headers tell the size, type and ETag.

Parameters

Get a media file's headers: parameters
NameTypeDescription
namerequiredpath · stringThe tag's name, percent-encoded: Languages%3A%3AGerman is Languages::German.
uquery · string
expquery · integer
sigquery · string

Example request

# Get a media file's headers (needs a key with the "read" scope)
curl -I "https://ankinize.com/api/v1/media/cat.png" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"

Response

200The file's headers

  • 304The ETag still matches
  • 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.
PUT/api/v1/media/{name}write

Store a media file#

Stores the request body as the file, creating it or replacing it. Send the bytes as they are (any Content-Type) with a Content-Length header; chunked uploads are refused with 411. A file is at most 100 MiB and not empty.

The name must be one Anki clients would keep as it is: at most 120 bytes of UTF-8, in Unicode NFC, with none of [ ] < > : " / ? * ^ \ | or control characters, no Windows device name (CON, NUL, LPT1...), and not ending in a space or a dot. A client rewrites any other name when it syncs, so the file would never settle between devices; the 422 invalid_media_name says the name to use. Thumbs.db and .DS_Store are never synced and are refused as well.

201 means the file is new; 200 means it replaced a file (created is false) or the bytes were the same (unchanged is true, and the usn stays). A new or changed file gets the next usn, so the media sync sends it to your devices. A file that would take the storage over the quota is 403 over_quota, and a collection already holding the most file names it may (deleted names count) is 403 media_entry_limit; replacing a name never adds one.

Parameters

Store a media file: parameters
NameTypeDescription
namerequiredpath · stringThe tag's name, percent-encoded: Languages%3A%3AGerman is Languages::German.

Example request

# Store a media file (needs a key with the "write" scope)
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
# --data-binary sends the file as it is and sets Content-Length.
curl -X PUT "https://ankinize.com/api/v1/media/cat.png" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @cat.png

Response

200The file replaced one, or the bytes were the same

Store a media file: response fields
NameTypeDescription
namerequiredstring
sizerequiredinteger
sha1requiredstring
sha256string | null
contentTyperequiredstring
mtimerequiredinteger
usnrequiredinteger
createdrequiredboolean
unchangedrequiredboolean
  • 400The file name cannot be decoded (invalid_media_name), or the body is not as long as Content-Length says (length_mismatch)
  • 403The key lacks the write scope, the account is suspended or unverified, the file would take the storage over its quota (over_quota), or the collection already holds the most file names it may (media_entry_limit)
  • 411No Content-Length header (length_required)
  • 413A media file is larger than 100 MiB (payload_too_large).
  • 422The name is not one Anki clients keep as it is (invalid_media_name), or the file is empty (empty_file)
  • 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/media/{name}write

Delete a media file#

Deletes the file. Your devices are told on their next media sync, as when a file is deleted in Anki. The storage it used is freed once no other name uses the same bytes. A name that is not there is 404.

Parameters

Delete a media file: parameters
NameTypeDescription
namerequiredpath · stringThe tag's name, percent-encoded: Languages%3A%3AGerman is Languages::German.

Example request

# Delete a media file (needs a key with the "write" scope)
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X DELETE "https://ankinize.com/api/v1/media/cat.png" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"

Response

200The file was deleted

Delete a media file: response fields
NameTypeDescription
okrequiredboolean
namerequiredstring
usnrequiredinteger
  • 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.

On this page

  • Quick start
  • Conventions
  • Base URL and format
  • Authentication and scopes
  • Errors
  • Rate limits
  • Retrying safely
  • Names and casing
  • When Anki is syncing
  • Pagination
  • Searching
  • Changes that need everyone to re-sync
  • Endpoints
  • Decks
  • GET /decks
  • POST /decks
  • GET /decks/{id}
  • PATCH /decks/{id}
  • DELETE /decks/{id}
  • Notes
  • GET /notes
  • POST /notes
  • POST /notes/batch-create
  • POST /notes/batch-update
  • POST /notes/batch-delete
  • GET /notes/{id}
  • PATCH /notes/{id}
  • DELETE /notes/{id}
  • Note types
  • GET /note-types
  • POST /note-types
  • GET /note-types/{id}
  • PATCH /note-types/{id}
  • DELETE /note-types/{id}
  • Tags
  • GET /tags
  • POST /tags
  • GET /tags/{name}
  • PATCH /tags/{name}
  • DELETE /tags/{name}
  • POST /tags/batch-create
  • POST /tags/batch-update
  • POST /tags/batch-delete
  • Cards
  • GET /cards
  • POST /cards/batch-update
  • POST /cards/batch-delete
  • GET /cards/{id}
  • PATCH /cards/{id}
  • DELETE /cards/{id}
  • GET /cards/{id}/render
  • GET /cards/{id}/scheduling
  • POST /cards/{id}/answer
  • Study
  • GET /queue
  • GET /stats
  • Media
  • GET /media
  • GET /media/{name}
  • HEAD /media/{name}
  • PUT /media/{name}
  • DELETE /media/{name}