# REST API

Read and change your Anki collection over HTTP. Everything here edits the same collection that Anki syncs, on Anki schema 18, so a note created through the API appears in Anki on its next sync and the other way round.

Every request is made as one user with an API key. While Anki is syncing that user's collection, writes wait: you get `423 Locked` with a `Retry-After` header, and nothing is lost if you retry.

## Quick start

Create an API key with the `write` scope under *API Keys & Devices* in the dashboard, keep it in an environment variable, and make your first request. This lists your decks:

```
# Keep the key out of your shell history and your code.
export ANKINIZE_API_KEY="paste-your-key-here"

# List every deck in your collection.
curl "https://ankinize.com/api/v1/decks" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"
```

The TypeScript examples on this page use the official [TypeScript SDK](https://ankinize.com/docs/sdk), which makes the same requests. Agents can load the [SDK skill or the REST skill](https://ankinize.com/docs/sdk#agents).

Then add a card. A note belongs to a deck and a note type, which you name (ignoring case; a nested deck is `Languages::German`). The note's `fields` is an object keyed by each field's name in camelCase: the field `Back Extra` is `backExtra`. Every field of a note type shows its key in [Get a note type](#getNoteType). You can send `notetypeId` and `deckId` instead. Reading a note gives its `fields` in the same shape, and editing one takes the same object with only the fields that change.

```
# Create a "Basic" note: the front and back of one card.
curl -X POST "https://ankinize.com/api/v1/notes" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "notetypeName": "Basic", "deckName": "Languages::German",
        "fields": { "front": "der Hund", "back": "the dog" },
        "tags": ["german"] }'
```

The card is in your collection straight away and reaches Anki on its next sync.

## Conventions

### Base URL and format

Every endpoint lives under `https://ankinize.com/api/v1`. Requests and responses are JSON; send `Content-Type: application/json` on anything with a body. A request body can be up to 8 MiB, and the fields of a single note up to 5 MiB together. Ids are numbers (Anki uses millisecond timestamps), so keep them as integers.

### Authentication and scopes

Send your key as `Authorization: Bearer <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](https://ankinize.com/docs/authentication) for how to create and limit keys.

### Errors

Failures use the HTTP status that fits and one JSON shape, so you can branch on `error.code` and show `error.message` to a person:

404 Not Found

```
{
  "error": {
    "code": "not_found",
    "message": "Deck 1736899200123 not found"
  }
}
```

| Status | Code | What happened |
| --- | --- | --- |
| `400` | `invalid_input` | The request is well formed but not valid: an unknown deck, a fields key the note type does not have. Also deck_error and card_type_error, with the same meaning. |
| `400` | `invalid_json` | The body is not valid JSON. |
| `401` | `unauthorized` | The key is missing, wrong, disabled or expired. |
| `403` | `forbidden` | The key lacks the scope, or the account is suspended or unverified. |
| `403` | `over_quota` | The account is over its storage quota. Every change except a delete is refused. |
| `403` | `destructive_change_disabled` | Removing a field, a card template or a note type is switched off for this account. |
| `404` | `not_found` | No such deck, note, note type, card or endpoint. |
| `409` | `full_sync_required` | The change forces a full sync on every device. Repeat it with acceptFullSync=true. |
| `413` | `payload_too_large` | The body is over 8 MiB. |
| `422` | `validation_error` | A field is missing or has the wrong type. The message names it. |
| `422` | `idempotency_conflict` | The Idempotency-Key was already used with a different body. |
| `423` | `locked` | Anki is syncing, or another write is running. Retry after the Retry-After seconds. |
| `429` | `rate_limited` | The plan's rate limit is used up. Wait the Retry-After seconds and send the same request again. |
| `500` | `internal_error` | Something went wrong on our side. The response carries a correlationId; quote it if you ask for help. |

> Validation errors are 422
> 
> The OpenAPI document lists `400` for a body that fails validation, but the server answers `422 validation_error`. Treat both as "fix the request".

### Rate limits

Every call made with an API key counts against your plan. Free allows 30 requests a minute and 1,000 a day, Pro 120 a minute, and Max 600 a minute. Minutes start on the clock, and the day ends at 00:00 UTC. Every answer carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds) for the limit you are closest to using up. Past it, the API answers `429 rate_limited` with a `Retry-After` header. Nothing was changed, so wait that long and send the same request again. A refused call does not count. Syncing with Anki is not limited this way, and a signed media link opened in a browser is not counted.

### Retrying safely

Networks fail halfway. Add an `Idempotency-Key` header (any unique string, a UUID is ideal) to a `POST`, `PATCH` or `DELETE` and a retry returns the first answer instead of doing the work twice. The answer is remembered for 24 hours, and a replay carries `X-Idempotent-Replay: true`. Reusing a key with a different body is a `422`. Failures that are worth retrying for real (`403`, `423`, `429`, `5xx`) are not remembered, so retrying them runs the request again.

### Names and casing

Every parameter and body field is camelCase (`notetypeId`, `deckName`, `nextCursor`), and so are the keys of a note's `fields`. A field's key is its name cut into words at spaces, punctuation and lower-to-upper changes, the first word in lower case and the others capitalised: `The field` is `theField`, `ab_x` is `abX`, `URLText` is `urlText`, and `Question 1` is `question1`. A name with no letter or digit is its own key, and when two fields would get the same key they both keep their exact names. The exact name, in any case, always works as a key too, and a key the note type does not have is a `400` that lists the valid ones. The one exception to camelCase is the HTTP header `Idempotency-Key`, which keeps the standard header name; the SDK takes it as `idempotencyKey` and sends the header for you. Tag names go in the URL path, so percent-encode what a path cannot hold (a space is `%20`, a slash `%2F`); a tag called `batch-create`, `batch-update` or `batch-delete` can only be reached through the batch endpoints.

### When Anki is syncing

While one of your devices is syncing, or another change is being written, the collection is locked and the API answers `423 Locked` with a `Retry-After` header (seconds). Nothing was changed, so wait that long and send the same request again.

### Pagination

`GET /notes` and `GET /cards` return pages in ascending id order. Take `nextCursor` from a response and pass it as `cursor` to get the next page; when it is `null` you have everything. `limit` is 1 to 100 and defaults to 50. A search with `q` can scan a bounded number of cards per request, so a page may be short or even empty while `nextCursor` is still set: keep following it.

```
# The first page. Then repeat with cursor=<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 match | Write | Example |
| --- | --- | --- |
| Note text | Words; quotes group | `"ice cream" dog` |
| Combine | `-`, `or`, `and`, parentheses | `dog or cat -tag:easy` |
| A field | `field:text`, `*:text` for any field | `front:dog front:*og front:re:^d` |
| Notes and cards | `deck:` `note:` `card:` `tag:` `preset:` `flag:` | `deck:Languages tag:vocab flag:1` |
| State | `is:new\|learn\|review\|due\|suspended\|buried\|buried-manually\|buried-sibling` | `is:due -is:suspended` |
| Time | `added:` `edited:` `introduced:` `rated:days[:button]` `resched:` | `added:7 rated:1:1` |
| Properties | `prop:` with `ivl`, `reps`, `lapses`, `ease`, `due`, `pos`, `rated`, and FSRS `s` `d` `r` | `prop:ivl>=21 prop:lapses>3` |
| Custom data | `prop:cdn:key>1`, `prop:cds:key=x`, `has-cd:key` | `has-cd:streak` |
| Ids | `nid:` `cid:` `did:` (a deck, filtered or not) `mid:` (a note type) | `did:1736899200123` |
| Duplicates | `dupe:notetypeId,text` | `dupe:1736899200123,dog` |
| Accents, clozes, words | `nc:` ignores accents, `sc:` looks inside clozes, `w:` a whole Unicode word | `nc:cafe` |
| Regular expression | `re:regex` | `re:^d.g$` |
| Typos (ours) | `fz:text` | `fz:japnese` |

### Differences from desktop Anki

- Regular expressions run on the RE2 engine. A `(?x)` flag, class set operations such as `[a-z&&[^aeiou]]`, backreferences and `\Q..\E` are `400 invalid_input`, and `\b` matches ASCII word characters only, so use `w:` for Unicode words.
- `fz:` is Ankinize's, not Anki's. The text and the note are cut into words (letters and digits, ignoring case and accents, with HTML and cloze markup removed), and every word of the search must match a word of the note, in any order. A word of up to 4 characters must match exactly, one of 5 to 8 may differ by one typo and a longer one by two; a typo is a missing, extra or wrong character, or two neighbours swapped. `fz:japnese` finds *Japanese*, and `fz:"dgo catt"` finds a note with *dog* and *cat* only if both are close enough. It answers yes or no, in id order, never by relevance, for up to 8 words of up to 64 characters. It also works as `front:fz:text` and `tag:fz:text`. As with `tag:` and `deck:`, a field named `fz` can no longer be searched by name.

### Notes and cards

A card matches when it satisfies the whole search. A note matches when *one of its cards* does, so `deck:Languages is:due` finds notes with a card that is in the deck *and* due. A search that asks nothing of the cards (`tag:`, a field, plain text) looks at the notes alone, so it also finds a note that has no cards. A search that has to look at every card or note, such as a regular expression, `prop:s` or `fz:`, stops after about 10,000 and returns what it found with a `nextCursor`; keep following it (see Pagination).

### Decks, tags and note types

On `GET /decks`, `GET /tags` and `GET /notetypes` the search filters names. Plain text matches anywhere in the name, ignoring case, with `*` and `_` as wildcards; `re:`, `nc:` and `fz:` work, and so does the list's own term: `deck:` on decks (`deck:Languages` is that deck and everything below it), `tag:` on tags (likewise) and `note:` on note types (`note:Basic*` matches whole names). A colon in plain text has to be escaped (`Languages\:\:German`). Any other term, such as `is:due`, is `400 invalid_input`.

### Changes that need everyone to re-sync

Adding or reordering a field or a template changes the collection's *schema*, and Anki then asks every device to download the whole collection once. So the API stops and asks first: the request fails with `409 full_sync_required` until you repeat it with `?acceptFullSync=true`.

**Removing** a field, a template or a whole note type loses data, so it also needs *Allow destructive schema changes via API* to be switched on under *Settings → Developer* in the dashboard. Otherwise you get `403 destructive_change_disabled`. Deleting decks and notes is not affected by that switch.

## Endpoints

| Endpoint | What it does | Scope |
| --- | --- | --- |
| [GET`/decks`](#listDecks) | List decks | `read` |
| [POST`/decks`](#createDeck) | Create a deck | `write` |
| [GET`/decks/{id}`](#getDeck) | Get a deck by ID | `read` |
| [PATCH`/decks/{id}`](#renameDeck) | Rename a deck | `write` |
| [DELETE`/decks/{id}`](#deleteDeck) | Delete a deck and its subdecks | `write` |
| [GET`/notes`](#listNotes) | Search notes | `read` |
| [POST`/notes`](#createNote) | Create a note and generate its cards | `write` |
| [POST`/notes/batch-create`](#createNotesBatch) | Create many notes in one call | `write` |
| [POST`/notes/batch-update`](#updateNotesBatch) | Edit many notes in one call | `write` |
| [POST`/notes/batch-delete`](#deleteNotesBatch) | Delete many notes in one call | `write` |
| [GET`/notes/{id}`](#getNote) | Get note by ID with cards | `read` |
| [PATCH`/notes/{id}`](#updateNote) | Update note fields or tags | `write` |
| [DELETE`/notes/{id}`](#deleteNote) | Delete a note and its cards | `write` |
| [GET`/note-types`](#listNoteTypes) | List note types | `read` |
| [POST`/note-types`](#createNoteType) | Create note type (stock or custom) | `write` |
| [GET`/note-types/{id}`](#getNoteType) | Get note type details with fields and templates | `read` |
| [PATCH`/note-types/{id}`](#updateNoteType) | Update note type fields, templates, or styling | `write` |
| [DELETE`/note-types/{id}`](#deleteNoteType) | Delete a note type | `write` |
| [GET`/queue`](#getStudyQueue) | Get the study queue of a deck | `read` |
| [GET`/tags`](#listTags) | List tags | `read` |
| [POST`/tags`](#createTag) | Create a tag | `write` |
| [GET`/tags/{name}`](#getTag) | Get a tag by name | `read` |
| [PATCH`/tags/{name}`](#renameTag) | Rename a tag | `write` |
| [DELETE`/tags/{name}`](#deleteTag) | Delete a tag | `write` |
| [POST`/tags/batch-create`](#createTagsBatch) | Create many tags in one call | `write` |
| [POST`/tags/batch-update`](#updateTagsBatch) | Rename many tags in one call | `write` |
| [POST`/tags/batch-delete`](#deleteTagsBatch) | Delete many tags in one call | `write` |
| [GET`/stats`](#getStats) | Get the statistics of a search | `read` |
| [GET`/media`](#listMedia) | List media files | `read` |
| [GET`/media/{name}`](#getMedia) | Download a media file | `read` |
| [HEAD`/media/{name}`](#headMedia) | Get a media file's headers | `read` |
| [PUT`/media/{name}`](#putMedia) | Store a media file | `write` |
| [DELETE`/media/{name}`](#deleteMedia) | Delete a media file | `write` |
| [GET`/cards`](#listCards) | Search cards | `read` |
| [POST`/cards/batch-update`](#updateCardsBatch) | Edit many cards in one call | `write` |
| [POST`/cards/batch-delete`](#deleteCardsBatch) | Delete many cards in one call | `write` |
| [GET`/cards/{id}`](#getCard) | Get card details by ID | `read` |
| [PATCH`/cards/{id}`](#updateCard) | Edit a card | `write` |
| [DELETE`/cards/{id}`](#deleteCard) | Delete a card | `write` |
| [GET`/cards/{id}/render`](#renderCard) | Render a card's front and back | `read` |
| [GET`/cards/{id}/scheduling`](#getCardScheduling) | Preview the four answer buttons | `read` |
| [POST`/cards/{id}/answer`](#answerCard) | Answer (review) a card | `write` |

## Decks

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

GET`/api/v1/decks`read

### List decks

Returns every deck in the collection, including the built-in `Default` deck and nested decks, or only the decks whose name `q` accepts. A nested deck's name is its full path, such as `Languages::German`.

Parameters

**List decks: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `q` | `query · string` | Only the decks whose name this search accepts. Plain text matches anywhere in the full name (`Languages::German`), ignoring case, with `*` and `_` as wildcards. `deck:Languages` matches that deck and everything below it, `deck:Lang*` and `deck:*::German` use wildcards. `re:regex`, `nc:text` (ignore accents) and `fz:text` (typos allowed, see [Searching](https://ankinize.com/docs/api#search)) work, and `-`, `or`, `and`, quotes and parentheses combine terms. A colon in plain text has to be escaped (`Languages\:\:German`). Any other term is `400 invalid_input`. Leave it out for every deck. |

Example request

```
# List decks (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/decks" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"
```

Response

200List of decks

**List decks: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `decks`required | `Deck[]` | Every deck, in no particular order. |

200 response

```
{
  "decks": [
    {
      "id": 1,
      "name": "Default"
    },
    {
      "id": 1736899200123,
      "name": "Languages::German"
    }
  ]
}
```

- 401The `Authorization` header is missing, or the key is invalid, disabled or expired.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

POST`/api/v1/decks`write

### Create a deck

Creates a deck and returns it. Use `::` in the name to nest it under a parent, for example `Languages::German`.

Parameters

**Create a deck: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Create a deck: request body**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `string` | Deck name. Use `::` to nest it. |

Example body

```
{
  "name": "Languages::German"
}
```

Example request

```
# Create a deck (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/decks" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Languages::German"
  }'
```

Response

201Deck created

**Create a deck: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `integer` | Numeric id. Stable for the life of the deck. |
| `name`required | `string` | Full path, with `::` between parent and child. |

201 response

```
{
  "id": 1736899200123,
  "name": "Languages::German"
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 401The `Authorization` header is missing, or the key is invalid, disabled or expired.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`).
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

GET`/api/v1/decks/{id}`read

### Get a deck by ID

Returns one deck by its numeric id.

Parameters

**Get a deck by ID: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |

Example request

```
# Get a deck by ID (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/decks/1736899200123" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"
```

Response

200Deck details

**Get a deck by ID: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `integer` | Numeric id. Stable for the life of the deck. |
| `name`required | `string` | Full path, with `::` between parent and child. |

200 response

```
{
  "id": 1736899200123,
  "name": "Languages::German"
}
```

- 404No deck, note, note type or card has that id.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

PATCH`/api/v1/decks/{id}`write

### Rename a deck

Changes a deck's name. The deck keeps its id and its cards, and its subdecks are renamed with it.

Parameters

**Rename a deck: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Rename a deck: request body**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `string` | The new full name. |

Example body

```
{
  "name": "Languages::Deutsch"
}
```

Example request

```
# Rename a deck (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X PATCH "https://ankinize.com/api/v1/decks/1736899200123" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Languages::Deutsch"
  }'
```

Response

200Deck renamed

**Rename a deck: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `integer` | Numeric id. Stable for the life of the deck. |
| `name`required | `string` | Full path, with `::` between parent and child. |

200 response

```
{
  "id": 1736899200123,
  "name": "Languages::Deutsch"
}
```

- 404No deck, note, note type or card has that id.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

DELETE`/api/v1/decks/{id}`write

### Delete a deck and its subdecks

Deletes the deck, all of its subdecks and every card in them. Notes left with no cards are removed too.

This cannot be undone. The response lists exactly which decks and cards were removed. Deleting is still allowed while the account is over its storage quota.

Parameters

**Delete a deck and its subdecks: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Example request

```
# Delete a deck and its subdecks (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X DELETE "https://ankinize.com/api/v1/decks/1736899200123" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41"
```

Response

200Deck deleted

**Delete a deck and its subdecks: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `ok`required | `boolean` | `true` when the deck was deleted. |
| `deletedDeckIds`required | `integer[]` | The deck and each of its subdecks. |
| `deletedCardIds`required | `integer[]` | Every card that was in those decks. |
| `deletedNoteIds`required | `integer[]` | Notes that were left with no cards and were removed with them. |
| `defaultDeckReset`required | `boolean` | `true` when the built-in `Default` deck was among the decks. It cannot be removed, so it was emptied and kept. |
| `returnedCardIds`required | `integer[]` | Cards that were in a deleted filtered deck. They go back to the deck they came from instead of being deleted. |

200 response

```
{
  "ok": true,
  "deletedDeckIds": [
    1736899200123
  ],
  "defaultDeckReset": false,
  "deletedCardIds": [
    1736899300457,
    1736899300458
  ],
  "deletedNoteIds": [
    1736899300456
  ],
  "returnedCardIds": []
}
```

- 404No deck, note, note type or card has that id.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

## Notes

A note holds the content (its fields and tags). Anki turns each note into one or more cards using the note type's templates.

GET`/api/v1/notes`read

### Search notes

Returns the notes that match a search (`q`; every note when it is empty), in ascending id order, one page at a time. `q` is the search of [Searching](https://ankinize.com/docs/api#search), including `fz:` for typos: the Anki browser's notes mode. A note is found when one of its cards satisfies the whole search, so `deck:Languages is:due` needs a card that is in the deck *and* due. A search that asks nothing of the cards (`tag:`, a field, plain text) looks at the notes alone, so it also finds a note that has no cards.

To read the next page, send the previous response's `nextCursor` as `cursor`. `nextCursor` is `null` on the last page. As for cards, a search that has to look at every note, such as a regular expression or a fuzzy search, stops after about 10,000 notes and returns what it found so far with a `nextCursor`, so keep following it until it is `null`.

Parameters

**Search notes: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `q` | `query · string` | A search, written in Anki's search syntax: the text you would type in the browser, such as `deck:Languages is:due -tag:easy`. Leave it out for everything. The terms, the differences from desktop Anki, `fz:` for typos and the 1000-character limit are in [Searching](https://ankinize.com/docs/api#search); a search Anki would refuse is `400 invalid_input`. |
| `cursor` | `query · string` | The id of the last item on the previous page, which is its `nextCursor`. Leave it out for the first page. |
| `limit` | `query · integer` | Page size, from 1 to 100. Defaults to 50. |

Example request

```
# Search notes (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/notes?q=tag%3Avocab%20front%3Adog*&limit=50" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"
```

Response

200Paginated list of notes

**Search notes: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `notes`required | `Note[]` | This page of notes, in ascending id order. |
| `nextCursor`required | `string \| null` | Send this as `cursor` to get the next page. `null` on the last page. |

200 response

```
{
  "notes": [
    {
      "id": 1736899300456,
      "guid": "k3Jx9!aQ2c",
      "notetypeId": 1736899100001,
      "mtime": 1736899300,
      "usn": -1,
      "tags": [
        "vocab",
        "a1"
      ],
      "fields": {
        "front": "der Hund",
        "back": "the dog"
      }
    }
  ],
  "nextCursor": "1736899300456"
}
```

- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

POST`/api/v1/notes`write

### Create a note and generate its cards

Creates a note and generates its cards from the note type's templates.

Name the note type with `notetypeName` and the deck with `deckName` (compared ignoring case; a nested deck is `Languages::German`), or send `notetypeId` and `deckId` instead. Exactly one of `notetypeName` and `notetypeId` is needed; without a deck the note goes to the `Default` deck. A name that matches nothing is `404`.

`fields` is an object with one entry per field of the note type, keyed by the field's name in camelCase: `Back Extra` is `backExtra`. Each field of a note type shows its `key` in [Get a note type](#getNoteType). The field's exact name works as a key too. A field you leave out is empty; a key that names no field is `400 invalid_input` and lists the valid keys.

Send a `guid` only when you import notes from somewhere else and need to keep a stable identity; otherwise one is generated. All fields together may not exceed 5 MiB.

Parameters

**Create a note and generate its cards: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Create a note and generate its cards: request body**

| Name | Type | Description |
| --- | --- | --- |
| `notetypeName` | `string` | Which note type to use, by name (ignoring case). Send this or `notetypeId`. |
| `notetypeId` | `integer` | Which note type to use, by id. See [List note types](#listNoteTypes). Send this or `notetypeName`. |
| `deckName` | `string` | The deck that receives the new cards, by name (ignoring case), such as `Languages::German`. Send this or `deckId`. |
| `deckId` | `integer` | The deck that receives the new cards, by id. Without a deck the note goes to the `Default` deck. |
| `fields`required | `object` | The field contents, keyed by each field's key: its name in camelCase (`Back Extra` is `backExtra`) or its exact name. Fields left out are empty. HTML is allowed. |
| `tags` | `string[]` | Tags to put on the note. |
| `guid` | `string` | Optional. Only set it to keep an identity from another system. |

Example body

```
{
  "notetypeName": "Basic (and reversed card)",
  "deckName": "Languages::German",
  "fields": {
    "front": "der Hund",
    "back": "the dog"
  },
  "tags": [
    "vocab",
    "a1"
  ]
}
```

Example request

```
# Create a note and generate its cards (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/notes" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "notetypeName": "Basic (and reversed card)",
    "deckName": "Languages::German",
    "fields": {
      "front": "der Hund",
      "back": "the dog"
    },
    "tags": [
      "vocab",
      "a1"
    ]
  }'
```

Response

201Note created

**Create a note and generate its cards: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `integer` | Numeric id of the new note. |
| `guid`required | `string` | The note's globally unique id: the one you sent, or a generated one. |
| `cards`required | `GeneratedCard[]` | The cards the note type's templates produced. |
| `newTags`required | `string[]` | Tags that did not exist in the collection before and were created by this request. |

201 response

```
{
  "id": 1736899300456,
  "guid": "k3Jx9!aQ2c",
  "cards": [
    {
      "id": 1736899300457,
      "ord": 0,
      "deckId": 1736899200123,
      "due": 1
    },
    {
      "id": 1736899300458,
      "ord": 1,
      "deckId": 1736899200123,
      "due": 2
    }
  ]
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`).
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

POST`/api/v1/notes/batch-create`write

### Create many notes in one call

Creates up to 200 notes in one call. It does what [Create a note](#createNote) does for each entry (also naming note types and decks by name), but in one transaction and with the note types, decks and tags looked up once, so importing is far faster than one request per note.

The answer is `200` with one entry in `results` per note you sent, in the same order (`index`). An entry the collection refuses, such as an unknown deck or a key that names no field, is reported as `ok: false` with an `error`, and the others are still written. Send `atomic: true` to write all or nothing: if any entry is refused, nothing is written and the answer is `422` with the same body.

A batch counts as one request per 10 notes (at least one) against your plan's rate limit. At most 2 batches run at once on the server; when it is busy the answer is `503` with `Retry-After`, and nothing was changed.

Parameters

**Create many notes in one call: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Create many notes in one call: request body**

| Name | Type | Description |
| --- | --- | --- |
| `notes`required | `CreateNoteInput[]` | The notes to create, 1 to 200, each shaped like the body of [Create a note](#createNote). An entry that is not valid is reported in its slot. |
| `atomic` | `boolean` | Write all or nothing: one refused entry means nothing is written (`422`). |

Example body

```
{
  "notes": [
    {
      "notetypeName": "Basic (and reversed card)",
      "deckName": "Languages::German",
      "fields": {
        "front": "der Hund",
        "back": "the dog"
      },
      "tags": [
        "vocab",
        "a1"
      ]
    },
    {
      "notetypeName": "Basic (and reversed card)",
      "deckName": "Languages::German",
      "fields": {
        "front": "die Katze",
        "back": "the cat"
      }
    }
  ]
}
```

Example request

```
# Create many notes in one call (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/notes/batch-create" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "notes": [
      {
        "notetypeName": "Basic (and reversed card)",
        "deckName": "Languages::German",
        "fields": {
          "front": "der Hund",
          "back": "the dog"
        },
        "tags": [
          "vocab",
          "a1"
        ]
      },
      {
        "notetypeName": "Basic (and reversed card)",
        "deckName": "Languages::German",
        "fields": {
          "front": "die Katze",
          "back": "the cat"
        }
      }
    ]
  }'
```

Response

200Every entry was handled; see \`results\`

**Create many notes in one call: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `atomic`required | `boolean` |   |
| `committed`required | `boolean` |   |
| `created`required | `integer` |   |
| `failed`required | `integer` |   |
| `results`required | `CreateNotesBatchItem[]` |   |

200 response

```
{
  "atomic": false,
  "committed": true,
  "created": 2,
  "failed": 0,
  "results": [
    {
      "index": 0,
      "ok": true,
      "id": 1736899300001,
      "guid": "k3f9&aZ1xq",
      "cards": [
        {
          "id": 1736899300002,
          "ord": 0,
          "deckId": 1736899200123,
          "due": 1
        }
      ],
      "newTags": [
        "vocab",
        "a1"
      ]
    },
    {
      "index": 1,
      "ok": true,
      "id": 1736899300003,
      "guid": "p0L!m4Qe2c",
      "cards": [
        {
          "id": 1736899300004,
          "ord": 0,
          "deckId": 1736899200123,
          "due": 2
        }
      ],
      "newTags": []
    }
  ]
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 422The body failed validation (`validation_error`), or the call was `atomic` and an entry was refused: then the body is the usual batch answer with every entry's `error`, and nothing was written.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.
- 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed.

POST`/api/v1/notes/batch-update`write

### Edit many notes in one call

Edits up to 200 notes in one call. It does what [Edit a note](#updateNote) does for each entry, but in one transaction with note types, decks and tags looked up once. Each entry names a note by `id` and may send `fields`, `tags`, or the tag edits `addTags` and `removeTags`; what it leaves out stays as it was. `tags` replaces all tags and can not be sent with `addTags` or `removeTags`. `removeTags` works as in Anki: a tag is matched ignoring case, and removing `lang` also removes `lang::de` and the other tags below it (`*` is not a wildcard). A note may appear once per call.

The answer is `200` with one entry in `results` per entry you sent, in the same order (`index`). An entry the collection refuses, such as an unknown note or the wrong number of fields, is reported as `ok: false` with an `error`, and the others are still written. Send `atomic: true` to write all or nothing: if any entry is refused, nothing is written and the answer is `422` with the same body. `changed` counts notes that differed from what was stored.

A batch counts as one request per 10 entries (at least one) against your plan's rate limit.

Parameters

**Edit many notes in one call: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Edit many notes in one call: request body**

| Name | Type | Description |
| --- | --- | --- |
| `updates`required | `UpdateNotesBatchEntry[]` | The edits, 1 to 200. An entry that is not valid is reported in its slot. |
| `atomic` | `boolean` | Write all or nothing: one refused entry means nothing is written (`422`). |

Example body

```
{
  "updates": [
    {
      "id": 1736899300001,
      "fields": {
        "back": "the hound"
      }
    },
    {
      "id": 1736899300003,
      "addTags": [
        "animals"
      ],
      "removeTags": [
        "a1"
      ]
    }
  ]
}
```

Example request

```
# Edit many notes in one call (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/notes/batch-update" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "updates": [
      {
        "id": 1736899300001,
        "fields": {
          "back": "the hound"
        }
      },
      {
        "id": 1736899300003,
        "addTags": [
          "animals"
        ],
        "removeTags": [
          "a1"
        ]
      }
    ]
  }'
```

Response

200Every entry was handled; see \`results\`

**Edit many notes in one call: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `atomic`required | `boolean` |   |
| `committed`required | `boolean` |   |
| `changed`required | `integer` |   |
| `failed`required | `integer` |   |
| `results`required | `UpdateNotesBatchItem[]` |   |

200 response

```
{
  "atomic": false,
  "committed": true,
  "changed": 2,
  "failed": 0,
  "results": [
    {
      "index": 0,
      "ok": true,
      "id": 1736899300001,
      "changed": true,
      "newCards": [],
      "newTags": []
    },
    {
      "index": 1,
      "ok": true,
      "id": 1736899300003,
      "changed": true,
      "newCards": [],
      "newTags": [
        "animals"
      ]
    }
  ]
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 422The body failed validation (`validation_error`), or the call was `atomic` and an entry was refused: then the body is the usual batch answer with every entry's `error`, and nothing was written.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.
- 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed.

POST`/api/v1/notes/batch-delete`write

### Delete many notes in one call

Deletes up to 1000 notes and every card made from them, in one transaction. Ids that do not exist are not an error; they are listed in `missingNoteIds`. This cannot be undone. Deleting is still allowed while the account is over its storage quota.

A batch counts as one request per 10 ids (at least one) against your plan's rate limit.

Parameters

**Delete many notes in one call: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Delete many notes in one call: request body**

| Name | Type | Description |
| --- | --- | --- |
| `ids`required | `integer[]` | The notes to delete, 1 to 1000. |

Example body

```
{
  "ids": [
    1736899300001,
    1736899300003
  ]
}
```

Example request

```
# Delete many notes in one call (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/notes/batch-delete" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [
      1736899300001,
      1736899300003
    ]
  }'
```

Response

200The notes that existed were deleted

**Delete many notes in one call: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `deleted`required | `integer` | How many notes were deleted. |
| `deletedNoteIds`required | `integer[]` | The notes that were deleted. |
| `deletedCardIds`required | `integer[]` | Every card that was made from them. |
| `missingNoteIds`required | `integer[]` | Ids that did not exist and were ignored. |

200 response

```
{
  "deleted": 2,
  "deletedNoteIds": [
    1736899300001,
    1736899300003
  ],
  "deletedCardIds": [
    1736899300002,
    1736899300004
  ],
  "missingNoteIds": []
}
```

- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`).
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.
- 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed.

GET`/api/v1/notes/{id}`read

### Get note by ID with cards

Returns one note, including the cards it produced.

Parameters

**Get note by ID with cards: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |

Example request

```
# Get note by ID with cards (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/notes/1736899300456" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"
```

Response

200Note details

**Get note by ID with cards: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `integer` | Numeric id. Stable for the life of the note. |
| `guid`required | `string` | Globally unique id. Anki uses it to tell whether an imported note is the same one. |
| `notetypeId`required | `integer` | The note type this note was made with. |
| `mtime` | `integer` | When the note last changed, in seconds since 1970. |
| `usn` | `integer` | Update sequence number used by sync. `-1` means changed here and not yet synced. |
| `tags`required | `string[]` | Tags on the note. |
| `fields`required | `object` | Every field's content, keyed by the field's key (its name in camelCase, such as `backExtra`), in the order of the note type's fields. May contain HTML. |
| `cards` | `GeneratedCard[]` |   |

200 response

```
{
  "id": 1736899300456,
  "guid": "k3Jx9!aQ2c",
  "notetypeId": 1736899100001,
  "mtime": 1736899300,
  "usn": -1,
  "tags": [
    "vocab",
    "a1"
  ],
  "fields": {
    "front": "der Hund",
    "back": "the dog"
  },
  "cards": [
    {
      "id": 1736899300457,
      "ord": 0,
      "deckId": 1736899200123,
      "due": 1
    }
  ]
}
```

- 404No deck, note, note type or card has that id.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

PATCH`/api/v1/notes/{id}`write

### Update note fields or tags

Changes a note's fields, its tags, or both. Anything you leave out stays as it was, including each field missing from `fields`: send only the fields that change. If the new field content makes a template produce a card it did not before, the new cards are returned in `newCards`.

`changed` is `false` when the request matched what was already stored, and nothing is written.

Parameters

**Update note fields or tags: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Update note fields or tags: request body**

| Name | Type | Description |
| --- | --- | --- |
| `fields` | `object` | The fields to change, keyed by each field's key (its name in camelCase, such as `backExtra`) or its exact name. Fields left out keep their content. HTML is allowed. |
| `tags` | `string[]` | The complete list of tags; it replaces the existing tags. |

Example body

```
{
  "fields": {
    "back": "the dog (animal)"
  },
  "tags": [
    "vocab",
    "a1",
    "animals"
  ]
}
```

Example request

```
# Update note fields or tags (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X PATCH "https://ankinize.com/api/v1/notes/1736899300456" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": {
      "back": "the dog (animal)"
    },
    "tags": [
      "vocab",
      "a1",
      "animals"
    ]
  }'
```

Response

200Note updated

**Update note fields or tags: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `integer` | Numeric id of the note. |
| `changed`required | `boolean` | `false` when nothing differed from what was stored, so nothing was written. |
| `newCards`required | `GeneratedCard[]` | Cards that the new field content caused to be generated. |
| `newTags`required | `string[]` | Tags that did not exist in the collection before and were created by this request. |

200 response

```
{
  "id": 1736899300456,
  "changed": true,
  "newCards": [],
  "newTags": [
    "animals"
  ]
}
```

- 404No deck, note, note type or card has that id.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

DELETE`/api/v1/notes/{id}`write

### Delete a note and its cards

Deletes a note and every card made from it. This cannot be undone. Deleting is still allowed while the account is over its storage quota.

Parameters

**Delete a note and its cards: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Example request

```
# Delete a note and its cards (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X DELETE "https://ankinize.com/api/v1/notes/1736899300456" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41"
```

Response

200Note deleted

**Delete a note and its cards: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `ok`required | `boolean` | `true` when the note was deleted. |
| `deletedNoteIds`required | `integer[]` | The note that was deleted. |
| `deletedCardIds`required | `integer[]` | Every card that was made from it. |

200 response

```
{
  "ok": true,
  "deletedNoteIds": [
    1736899300456
  ],
  "deletedCardIds": [
    1736899300457,
    1736899300458
  ]
}
```

- 404No deck, note, note type or card has that id.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

## Note types

A note type is the template for notes: its fields, and the card templates that decide which cards a note produces.

GET`/api/v1/note-types`read

### List note types

Returns the id and name of every note type, or only the ones whose name `q` accepts. Use [Get a note type](#getNoteType) for its fields and templates.

Parameters

**List note types: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `q` | `query · string` | Only the note types whose name this search accepts. Plain text matches anywhere in the name, ignoring case, with `*` and `_` as wildcards. `note:Basic*` matches whole names (the way it does in a card search), and `re:regex`, `nc:text` and `fz:text` work. `-`, `or`, `and`, quotes and parentheses combine terms. Any other term is `400 invalid_input`. Leave it out for every note type. |

Example request

```
# List note types (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/note-types" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"
```

Response

200List of note types

**List note types: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `noteTypes`required | `object[]` | Every note type, with its id and name. Fetch one by id for its fields and templates. |

200 response

```
{
  "noteTypes": [
    {
      "id": 1736899100001,
      "name": "Basic"
    },
    {
      "id": 1736899100002,
      "name": "Cloze"
    }
  ]
}
```

- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

POST`/api/v1/note-types`write

### Create note type (stock or custom)

Creates a note type, either from one of Anki's stock designs or from your own fields and templates.

Send `stock` (`basic`, `basic-and-reversed`, `basic-optional-reversed`, `basic-type-in-answer`, `cloze` or `image-occlusion`) to start from a stock design, and optionally `name` to rename it. Or send `fields` and `templates` to define it yourself.

Parameters

**Create note type (stock or custom): parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Create note type (stock or custom): request body**

| Name | Type | Description |
| --- | --- | --- |
| `stock` | `string` | Start from one of Anki's stock designs. |
| `name` | `string` | Name of the note type. |
| `kind` | `"normal" \| "cloze"` |   |
| `fields` | `object[]` | Field definitions, in order. Each needs a `name`. |
| `templates` | `object[]` | Card templates. Each needs a `name`, a question format `qFormat` and an answer format `aFormat`. |
| `css` | `string` |   |
| `sortFieldIdx` | `integer` |   |

Example body

```
{
  "stock": "basic-and-reversed",
  "name": "Vocabulary"
}
```

Example request

```
# Create note type (stock or custom) (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/note-types" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "stock": "basic-and-reversed",
    "name": "Vocabulary"
  }'
```

Response

201Note type created

**Create note type (stock or custom): response fields**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `integer` | Numeric id of the new note type. |
| `name`required | `string` | Its name. |

201 response

```
{
  "id": 1736899400001,
  "name": "Vocabulary"
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

GET`/api/v1/note-types/{id}`read

### Get note type details with fields and templates

Returns a note type with its fields and card templates, its CSS and its sort field.

Parameters

**Get note type details with fields and templates: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |

Example request

```
# Get note type details with fields and templates (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/note-types/1736899100001" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"
```

Response

200Note type details

**Get note type details with fields and templates: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `integer` |   |
| `name`required | `string` |   |
| `kind`required | `"normal" \| "cloze"` | `normal` makes one card per template; `cloze` makes one per cloze deletion. |
| `css` | `string` | Stylesheet shared by every card of this note type. |
| `sortFieldIdx` | `integer` | Index of the field the browser sorts by. |
| `latexPre` | `string` |   |
| `latexPost` | `string` |   |
| `latexSvg` | `boolean` |   |
| `fields`required | `NoteTypeField[]` |   |
| `templates`required | `object[]` |   |

200 response

```
{
  "id": 1736899100001,
  "name": "Basic",
  "kind": "normal",
  "css": ".card { font-family: arial; font-size: 20px; text-align: center; }",
  "sortFieldIdx": 0,
  "fields": [
    {
      "ord": 0,
      "name": "Front",
      "key": "front"
    },
    {
      "ord": 1,
      "name": "Back",
      "key": "back"
    }
  ],
  "templates": [
    {
      "ord": 0,
      "name": "Card 1",
      "qFormat": "{{Front}}",
      "aFormat": "{{FrontSide}}<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**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |
| `acceptFullSync` | `query · boolean` | Set to `true` to accept that this change forces every device to download the whole collection again on its next sync. Without it the request is refused with `409 full_sync_required`. |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Update note type fields, templates, or styling: request body**

| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` |   |
| `css` | `string` |   |
| `sortFieldIdx` | `integer` |   |
| `fields` | `object[]` | The full list of fields. A field with an `ord` is an existing one being kept or renamed; one without is new; an existing one that is missing is removed. |
| `templates` | `object[]` | The full list of templates, following the same rule as `fields`. |

Example body

```
{
  "css": ".card { font-size: 24px; }"
}
```

Example request

```
# Update note type fields, templates, or styling (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X PATCH "https://ankinize.com/api/v1/note-types/1736899100001?acceptFullSync=true" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "css": ".card { font-size: 24px; }"
  }'
```

Response

200Note type updated

**Update note type fields, templates, or styling: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `integer` | Numeric id of the note type. |
| `name`required | `string` | Its name after the change. |
| `schemaChanged`required | `boolean` | `true` when the change altered the collection's schema, so every device downloads the whole collection on its next sync. |
| `change`required | `NotetypeChange` | What the change did to the note type's structure: how many fields and templates were added, removed or moved. |
| `updatedNotes`required | `integer` | Notes that were rewritten because a field was added, removed or moved, or the sort field changed. |
| `deletedCards`required | `integer` | Cards that were deleted because their template was removed. |
| `movedCards`required | `integer` | Cards whose template moved to a different position, so their number changed. |
| `newCards`required | `integer` | Cards that were generated because a template was added or now produces a card. |

200 response

```
{
  "id": 1736899100001,
  "name": "Basic",
  "schemaChanged": false,
  "change": {
    "fieldsAdded": 0,
    "fieldsRemoved": 0,
    "fieldsMoved": 0,
    "sortFieldChanged": false,
    "templatesAdded": 0,
    "templatesRemoved": 0,
    "templatesMoved": 0,
    "destructive": false
  },
  "updatedNotes": 0,
  "deletedCards": 0,
  "movedCards": 0,
  "newCards": 0
}
```

- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 404No deck, note, note type or card has that id.
- 409The change alters the collection's schema and needs `?acceptFullSync=true` (`full_sync_required`), or the card is in a state that does not allow this (`card_state_error`: suspended, buried, in a filtered deck, or an unsupported scheduler).
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

DELETE`/api/v1/note-types/{id}`write

### Delete a note type

Deletes a note type and every note that uses it.

This changes the collection's schema, so it needs `?acceptFullSync=true` (otherwise `409 full_sync_required`) and *Allow destructive schema changes via API* turned on under Settings → Developer (otherwise `403 destructive_change_disabled`).

Parameters

**Delete a note type: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |
| `acceptFullSync` | `query · boolean` | Set to `true` to accept that this change forces every device to download the whole collection again on its next sync. Without it the request is refused with `409 full_sync_required`. |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Example request

```
# Delete a note type (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X DELETE "https://ankinize.com/api/v1/note-types/1736899100001?acceptFullSync=true" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41"
```

Response

200Note type deleted

**Delete a note type: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `ok`required | `boolean` | `true` when the note type was deleted. |
| `deletedNotes`required | `integer` | Notes that used the note type and were removed. |
| `deletedCards`required | `integer` | Cards of those notes, removed with them. |
| `currentNotetypeId`required | `integer` | The note type Anki now treats as current: the first by name, or a new Basic one if none were left. |
| `addedBasic`required | `boolean` | `true` when that was the last note type, so a stock Basic one was added. |

200 response

```
{
  "ok": true,
  "deletedNotes": 42,
  "deletedCards": 42,
  "currentNotetypeId": 1736899100002,
  "addedBasic": false
}
```

- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 404No deck, note, note type or card has that id.
- 409The change alters the collection's schema and needs `?acceptFullSync=true` (`full_sync_required`), or the card is in a state that does not allow this (`card_state_error`: suspended, buried, in a filtered deck, or an unsupported scheduler).
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

## Tags

The labels notes carry. A tag is created when a note brings it, or here with no note yet. List them, rename a tag (its nested tags follow) or delete it from every note. Nested tags keep Anki's `::` separator; a tag name is one path segment, so percent-encode it (`Languages%3A%3AGerman`). The names `batch-create`, `batch-update` and `batch-delete` belong to the batch endpoints: a tag with such a name can only be reached through them.

GET`/api/v1/tags`read

### List tags

Returns every tag in the collection, sorted by name, or only the tags `q` accepts. A nested tag keeps Anki's `::` separator, such as `Languages::German`; searching `tag:Languages` also finds the notes tagged with the nested ones.

Parameters

**List tags: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `q` | `query · string` | Only the tags this search accepts. Plain text matches anywhere in the tag, ignoring case, with `*` and `_` as wildcards. `tag:Languages` matches that tag and the tags below it (`Languages::German`); `tag:re:regex`, `tag:nc:text` and `tag:fz:text` work as in a card search, and `re:`, `nc:` and `fz:` without `tag:` look at the whole name. `-`, `or`, `and`, quotes and parentheses combine terms. Any other term is `400 invalid_input`. Leave it out for every tag. |

Example request

```
# List tags (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/tags" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"
```

Response

200List of tags

**List tags: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `tags`required | `string[]` | Every tag, sorted by name. |

200 response

```
{
  "tags": [
    "german",
    "Languages::German",
    "vocab"
  ]
}
```

- 401The `Authorization` header is missing, or the key is invalid, disabled or expired.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

POST`/api/v1/tags`write

### Create a tag

Creates a tag without putting it on a note, as Anki registers a tag when a note brings it. A name that is already used (ignoring case) answers `200` with `created: false` and the spelling that is stored, so sending it twice is safe. A nested name keeps the spelling of a parent that exists, as in Anki.

A tag no note carries is removed by Anki's *Check Database* and *Clear Unused Tags*.

Parameters

**Create a tag: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Create a tag: request body**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `string` | The tag, with `::` for nesting. It can not contain a space or have an empty part. |

Example body

```
{
  "name": "Languages::German"
}
```

Example request

```
# Create a tag (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/tags" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Languages::German"
  }'
```

Response

200The tag already existed

**Create a tag: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `string` | The tag, spelled as it is stored. |
| `created`required | `boolean` | `false` when the tag already existed. |

200 response

```
{
  "name": "Languages::German",
  "created": true
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`).
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

GET`/api/v1/tags/{name}`read

### Get a tag by name

Returns a tag and `noteCount`, the notes that carry it or one of its nested tags (the notes `tag:name` finds). A tag exists when it is in the list, when a nested tag is, or when a note carries it. The `name` in the path is the tag, percent-encoded, and is compared ignoring case.

Parameters

**Get a tag by name: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `path · string` | The tag's name, percent-encoded: `Languages%3A%3AGerman` is `Languages::German`. |

Example request

```
# Get a tag by name (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/tags/german" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"
```

Response

200Tag details

**Get a tag by name: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `string` | The tag, spelled as it is stored. |
| `noteCount`required | `integer` | How many notes carry this tag or a tag nested under it. |

200 response

```
{
  "name": "Languages::German",
  "noteCount": 42
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 404No deck, note, note type or card has that id.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

PATCH`/api/v1/tags/{name}`write

### Rename a tag

Renames a tag and every tag nested under it on all notes: renaming `Languages` to `Lang` makes `Languages::German` into `Lang::German`. The notes that change get a new modification time, so they sync. If the new name is already a tag, the two are merged, and a note that ends up with the tag twice keeps it once. Renaming a tag that no note carries renames it in the list.

`notes` is how many notes were looked at and changed.

Parameters

**Rename a tag: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `path · string` | The tag's name, percent-encoded: `Languages%3A%3AGerman` is `Languages::German`. |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Rename a tag: request body**

| Name | Type | Description |
| --- | --- | --- |
| `newName`required | `string` | The new name. If it is already a tag, the two are merged. |

Example body

```
{
  "newName": "Lang::German"
}
```

Example request

```
# Rename a tag (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X PATCH "https://ankinize.com/api/v1/tags/german" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "newName": "Lang::German"
  }'
```

Response

200Tag renamed

**Rename a tag: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `string` | The tag's new name, as stored. |
| `notes`required | `integer` | How many notes were changed. |

200 response

```
{
  "name": "Lang::German",
  "notes": 42
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 404No deck, note, note type or card has that id.
- 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`).
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

DELETE`/api/v1/tags/{name}`write

### Delete a tag

Removes a tag and the tags nested under it from every note and from the list. The notes keep everything else. This cannot be undone. Deleting is still allowed while the account is over its storage quota.

Parameters

**Delete a tag: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `path · string` | The tag's name, percent-encoded: `Languages%3A%3AGerman` is `Languages::German`. |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Example request

```
# Delete a tag (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X DELETE "https://ankinize.com/api/v1/tags/german" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41"
```

Response

200Tag deleted

**Delete a tag: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `ok`required | `boolean` |   |
| `deletedTags`required | `string[]` | The tags that were removed from the list: the one named and those nested under it. |
| `notes`required | `integer` | How many notes lost a tag. |

200 response

```
{
  "ok": true,
  "deletedTags": [
    "Lang::German",
    "Lang::German::Verbs"
  ],
  "notes": 57
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 404No deck, note, note type or card has that id.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

POST`/api/v1/tags/batch-create`write

### Create many tags in one call

Creates up to 200 tags in one call, in one transaction. It does what [Create a tag](#createTag) does for each name. The answer is `200` with one entry in `results` per name, in order; a name that is refused (such as one with a space) is `ok: false` with an `error` and the others are still created. Send `atomic: true` to write all or nothing: then the answer is `422` with the same body.

A batch counts as one request per 10 names against your plan's rate limit.

Parameters

**Create many tags in one call: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Create many tags in one call: request body**

| Name | Type | Description |
| --- | --- | --- |
| `names`required | `string[]` | The tags to create, 1 to 200. |
| `atomic` | `boolean` | Write all or nothing. See [Create many notes](#createNotesBatch). |

Example body

```
{
  "names": [
    "vocab",
    "grammar",
    "two words"
  ]
}
```

Example request

```
# Create many tags in one call (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/tags/batch-create" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "names": [
      "vocab",
      "grammar",
      "two words"
    ]
  }'
```

Response

200Every entry was handled; see \`results\`

**Create many tags in one call: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `atomic`required | `boolean` | Echoes the request. |
| `committed`required | `boolean` | Whether anything was written. |
| `created`required | `integer` | How many tags were new. |
| `failed`required | `integer` | How many entries were refused. |
| `results`required | `CreateTagsBatchItem[]` | One entry per name sent, in order. |

200 response

```
{
  "atomic": false,
  "committed": true,
  "created": 2,
  "failed": 1,
  "results": [
    {
      "index": 0,
      "ok": true,
      "name": "vocab",
      "created": true
    },
    {
      "index": 1,
      "ok": true,
      "name": "grammar",
      "created": true
    },
    {
      "index": 2,
      "name": "two words",
      "ok": false,
      "error": {
        "code": "invalid_input",
        "message": "a tag name can not contain a space"
      }
    }
  ]
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 422The body failed validation (`validation_error`), or the call was `atomic` and an entry was refused: then the body is the usual batch answer with every entry's `error`, and nothing was written.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.
- 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed.

POST`/api/v1/tags/batch-update`write

### Rename many tags in one call

Renames up to 200 tags in one call, in one pass over the notes. Each entry names a tag by `name` and gives its `newName`, as [Rename a tag](#renameTag) does. Two entries may use the same `newName` (that merges the tags), but an entry that touches another one (a `name` that is, contains or lies inside another entry's `name` or `newName`) is refused with `invalid_input`, because the result would depend on the order. Entries are reported in `results` as in [Create many notes](#createNotesBatch); `atomic: true` writes all or nothing.

Parameters

**Rename many tags in one call: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Rename many tags in one call: request body**

| Name | Type | Description |
| --- | --- | --- |
| `updates`required | `UpdateTagsBatchEntry[]` | The renames, 1 to 200. |
| `atomic` | `boolean` | Write all or nothing. See [Create many notes](#createNotesBatch). |

Example body

```
{
  "updates": [
    {
      "name": "vocab",
      "newName": "vocabulary"
    },
    {
      "name": "grammar",
      "newName": "Grammar"
    }
  ]
}
```

Example request

```
# Rename many tags in one call (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/tags/batch-update" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "updates": [
      {
        "name": "vocab",
        "newName": "vocabulary"
      },
      {
        "name": "grammar",
        "newName": "Grammar"
      }
    ]
  }'
```

Response

200Every entry was handled; see \`results\`

**Rename many tags in one call: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `atomic`required | `boolean` |   |
| `committed`required | `boolean` | Whether anything was written. |
| `renamed`required | `integer` | How many tags were renamed. |
| `failed`required | `integer` | How many entries were refused. |
| `results`required | `UpdateTagsBatchItem[]` | One entry per update sent, in order. |

200 response

```
{
  "atomic": false,
  "committed": true,
  "renamed": 2,
  "failed": 0,
  "results": [
    {
      "index": 0,
      "ok": true,
      "name": "vocab",
      "newName": "vocabulary",
      "notes": 12
    },
    {
      "index": 1,
      "ok": true,
      "name": "grammar",
      "newName": "Grammar",
      "notes": 4
    }
  ]
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 422The body failed validation (`validation_error`), or the call was `atomic` and an entry was refused: then the body is the usual batch answer with every entry's `error`, and nothing was written.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.
- 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed.

POST`/api/v1/tags/batch-delete`write

### Delete many tags in one call

Deletes up to 1000 tags, with the tags nested under them, from every note and from the list, in one transaction. Names that do not exist are not an error; they are listed in `missingNames`. This cannot be undone. Deleting is still allowed while the account is over its storage quota.

Parameters

**Delete many tags in one call: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Delete many tags in one call: request body**

| Name | Type | Description |
| --- | --- | --- |
| `names`required | `string[]` | The tags to delete, 1 to 1000. |

Example body

```
{
  "names": [
    "vocabulary",
    "unused"
  ]
}
```

Example request

```
# Delete many tags in one call (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/tags/batch-delete" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "names": [
      "vocabulary",
      "unused"
    ]
  }'
```

Response

200The tags that existed were deleted

**Delete many tags in one call: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `deleted`required | `integer` | How many tags were removed from the list. |
| `deletedTags`required | `string[]` | The tags that were removed: those named and those nested under them. |
| `notes`required | `integer` | How many notes lost a tag. |
| `missingNames`required | `string[]` | Names that matched nothing and were ignored. |

200 response

```
{
  "deleted": 1,
  "deletedTags": [
    "vocabulary"
  ],
  "notes": 12,
  "missingNames": [
    "unused"
  ]
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.
- 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed.

## Cards

A card is one thing to review, made from a note. Change what a card shows by editing its note; here you can search, flag, suspend, bury, move and delete cards, render them for display, and answer them with Anki's v3 scheduler (SM-2 or FSRS, as the deck's preset says).

GET`/api/v1/cards`read

### Search cards

Returns the cards that match a search (`q`; every card when it is empty), in ascending id order, one page at a time. Narrow it by deck or note with `deck:`, `did:` and `nid:` in the search. Paginate with `nextCursor` as for notes, with one difference: a search that has to look at every card, such as a regular expression or `prop:s`, stops after about 10,000 cards and returns what it found so far together with a `nextCursor`. A page can therefore hold fewer than `limit` cards, or none, while `nextCursor` is not `null`; keep following it until it is.

Parameters

**Search cards: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `q` | `query · string` | A search, written in Anki's search syntax: the text you would type in the browser, such as `deck:Languages is:due -tag:easy`. Leave it out for everything. The terms, the differences from desktop Anki, `fz:` for typos and the 1000-character limit are in [Searching](https://ankinize.com/docs/api#search); a search Anki would refuse is `400 invalid_input`. |
| `cursor` | `query · string` | The id of the last item on the previous page, which is its `nextCursor`. Leave it out for the first page. |
| `limit` | `query · integer` | Page size, from 1 to 100. Defaults to 50. |

Example request

```
# Search cards (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/cards?q=deck%3ALanguages%3A%3AGerman%20is%3Adue&limit=50" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"
```

Response

200Paginated list of cards

**Search cards: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `cards`required | `Card[]` | This page of cards, in ascending id order. |
| `nextCursor`required | `string \| null` | Send this as `cursor` to get the next page. `null` when the search is complete. While it is not `null` the page may hold fewer than `limit` cards, or none. |

200 response

```
{
  "cards": [
    {
      "id": 1736899300457,
      "noteId": 1736899300456,
      "deckId": 1736899200123,
      "ord": 0,
      "mtime": 1736899300,
      "usn": -1,
      "type": 0,
      "queue": 0,
      "due": 1,
      "interval": 0,
      "factor": 0,
      "reps": 0,
      "lapses": 0,
      "left": 0,
      "originalDue": 0,
      "originalDeckId": 0,
      "flags": 0
    }
  ],
  "nextCursor": null
}
```

- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

POST`/api/v1/cards/batch-update`write

### Edit many cards in one call

Applies one change to up to 1000 cards, in one transaction: set a flag, move them to a deck, suspend or bury them. It does what [Edit a card](#updateCard) does for each id, with the same rules for `patch`.

The answer is `200` with one entry in `results` per distinct id. A card that does not exist, or that sits in a filtered deck and so can not be moved, is reported as `ok: false`, and the others are still changed. Send `atomic: true` to write all or nothing: if any card is refused, nothing is written and the answer is `422` with the same body. `changed` counts the cards that differed from what was stored.

A batch counts as one request per 10 ids (at least one) against your plan's rate limit.

Parameters

**Edit many cards in one call: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Edit many cards in one call: request body**

| Name | Type | Description |
| --- | --- | --- |
| `ids`required | `integer[]` | The cards to change, 1 to 1000. Repeats are ignored. |
| `patch`required | `UpdateCardInput` | The change, shaped like the body of [Edit a card](#updateCard). |
| `atomic` | `boolean` | Write all or nothing: one refused card means nothing is written (`422`). |

Example body

```
{
  "ids": [
    1736899300002,
    1736899300004
  ],
  "patch": {
    "flag": 2
  }
}
```

Example request

```
# Edit many cards in one call (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/cards/batch-update" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [
      1736899300002,
      1736899300004
    ],
    "patch": {
      "flag": 2
    }
  }'
```

Response

200Every card was handled; see \`results\`

**Edit many cards in one call: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `atomic`required | `boolean` |   |
| `committed`required | `boolean` |   |
| `changed`required | `integer` |   |
| `failed`required | `integer` |   |
| `results`required | `UpdateCardsBatchItem[]` |   |

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 422The body failed validation (`validation_error`), or the call was `atomic` and an entry was refused: then the body is the usual batch answer with every entry's `error`, and nothing was written.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.
- 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed.

POST`/api/v1/cards/batch-delete`write

### Delete many cards in one call

Deletes up to 1000 cards in one transaction. A note whose last card is deleted is deleted with it. Ids that do not exist are not an error; they are listed in `missingCardIds`. This cannot be undone. Deleting is still allowed while the account is over its storage quota.

A batch counts as one request per 10 ids (at least one) against your plan's rate limit.

Parameters

**Delete many cards in one call: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Delete many cards in one call: request body**

| Name | Type | Description |
| --- | --- | --- |
| `ids`required | `integer[]` | The cards to delete, 1 to 1000. |

Example body

```
{
  "ids": [
    1736899300002,
    1736899300004
  ]
}
```

Example request

```
# Delete many cards in one call (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X POST "https://ankinize.com/api/v1/cards/batch-delete" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [
      1736899300002,
      1736899300004
    ]
  }'
```

Response

200The cards that existed were deleted

**Delete many cards in one call: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `deleted`required | `integer` | How many cards were deleted. |
| `deletedCardIds`required | `integer[]` | The cards that were deleted. |
| `deletedNoteIds`required | `integer[]` | Notes that were deleted because their last card went. |
| `missingCardIds`required | `integer[]` | Ids that did not exist and were ignored. |

200 response

```
{
  "deleted": 2,
  "deletedCardIds": [
    1736899300002,
    1736899300004
  ],
  "deletedNoteIds": [
    1736899300001,
    1736899300003
  ],
  "missingCardIds": []
}
```

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 403The key lacks the scope, the account is suspended or its email unverified, the account is over its storage quota (`over_quota`; deleting is still allowed), or a destructive schema change is switched off (`destructive_change_disabled`).
- 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`).
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.
- 503Other batch operations are using the server. Wait the number of seconds in `Retry-After` and retry; nothing was changed.

GET`/api/v1/cards/{id}`read

### Get card details by ID

Returns one card with its scheduling state: queue, due, interval, ease factor, repetitions and lapses.

Parameters

**Get card details by ID: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |

Example request

```
# Get card details by ID (needs a key with the "read" scope)
curl "https://ankinize.com/api/v1/cards/1736899300457" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY"
```

Response

200Card details

**Get card details by ID: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `integer` |   |
| `noteId`required | `integer` | The note this card was made from. |
| `deckId`required | `integer` |   |
| `ord`required | `integer` | Which template of the note type made this card (0 is the first). |
| `mtime` | `integer` |   |
| `usn` | `integer` |   |
| `type` | `integer` | 0 new, 1 learning, 2 review, 3 relearning. |
| `queue` | `integer` | Where the card sits in the scheduler: -3 or -2 buried, -1 suspended, 0 new, 1 learning, 2 review, 3 day-learning, 4 preview. |
| `due` | `integer` | When it is due. For a review card this is a day number; for a new card, its position in the queue. |
| `interval` | `integer` | Days until the next review, once it is a review card. |
| `factor` | `integer` | Ease factor in permille: 2500 means 250%. |
| `reps` | `integer` | How many times it has been reviewed. |
| `lapses` | `integer` | How many times it was forgotten after being learnt. |
| `left` | `integer` |   |
| `originalDue` | `integer` |   |
| `originalDeckId` | `integer` |   |
| `flags` | `integer` | Colour flag, 0 for none. |

- 404No deck, note, note type or card has that id.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

PATCH`/api/v1/cards/{id}`write

### Edit a card

Changes a card's flag, moves it to another deck, or suspends or buries it. Anything you leave out stays as it was.

`flag` is 0 (none) to 7. `suspended` and `buried` cannot both be sent. Suspending keeps a card out of review until you send `suspended: false`; burying hides it only until the next day or until you unbury it. A suspended card is never buried. Moving a card clears its FSRS memory state; it is replayed from the review history with the new deck's parameters the next time the card is shown or answered. Cards in a filtered deck can neither be moved nor moved into one (`409 card_state_error`).

`changed` is `false` when the request matched what was already stored.

Parameters

**Edit a card: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Edit a card: request body**

| Name | Type | Description |
| --- | --- | --- |
| `deckId` | `integer` |   |
| `flag` | `integer` |   |
| `suspended` | `boolean` |   |
| `buried` | `boolean` |   |

Example body

```
{
  "flag": 2,
  "suspended": false
}
```

Example request

```
# Edit a card (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X PATCH "https://ankinize.com/api/v1/cards/1736899300457" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41" \
  -H "Content-Type: application/json" \
  -d '{
    "flag": 2,
    "suspended": false
  }'
```

Response

200Card updated

**Edit a card: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `changed`required | `boolean` | `false` when nothing differed, so nothing was written. |
| `card`required | `Card` | The card as stored after the change. |

- 400The request was understood but is not valid, for example an unknown deck or a `fields` key that names no field of the note type. The code is `invalid_input`, `deck_error` or `card_type_error`.
- 404No deck, note, note type or card has that id.
- 422The body does not match the earlier request that used this `Idempotency-Key` (`idempotency_conflict`), or the body failed validation (`validation_error`).
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

DELETE`/api/v1/cards/{id}`write

### Delete a card

Deletes one card. If it was the last card of its note, the note is removed too. This cannot be undone. Deleting is still allowed while the account is over its storage quota.

Parameters

**Delete a card: parameters**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Example request

```
# Delete a card (needs a key with the "write" scope)
# Idempotency-Key makes a retry safe: the first answer is replayed.
# A 423 means Anki is syncing: wait Retry-After seconds, then retry.
curl -X DELETE "https://ankinize.com/api/v1/cards/1736899300457" \
  -H "Authorization: Bearer $ANKINIZE_API_KEY" \
  -H "Idempotency-Key: 9b2f6c1e-7f3a-4a53-9f55-0d3c1c2a8e41"
```

Response

200Card deleted

**Delete a card: response fields**

| Name | Type | Description |
| --- | --- | --- |
| `ok`required | `boolean` | `true` when the card was deleted. |
| `deletedCardIds`required | `integer[]` | The card that was deleted. |
| `deletedNoteIds`required | `integer[]` | The note, if the deleted card was its last one. |

- 404No deck, note, note type or card has that id.
- 423Another write or an Anki sync is using the collection. Wait the number of seconds in `Retry-After` and retry; nothing was changed.
- 429The plan's rate limit is used up: a limit per minute on every plan, and a limit per day on Free. Nothing was changed. Wait Retry-After seconds and send the same request again.

GET`/api/v1/cards/{id}/render`read

### Render a card's front and back

Renders the card as Anki shows it: the note's fields are put into the card template, so the question and the answer are ready to display. The answer includes the question above a divider, as in Anki.

Everything is sanitized on the server before it is returned: scripts, event handlers, iframes, forms, `javascript:` links and remote styles are removed, and CSS is limited to a safe subset. The result is still *content from your collection*, so display it in a sandboxed `<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**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |
| `format` | `query · string` | `json` (default) or `html` for one complete, sandboxed HTML document. |
| `typedAnswer` | `query · string` | What the learner typed, for a `{{type:Field}}` comparison. At most 1000 characters. |
| `mediaBaseUrl` | `query · string` | An `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**

| Name | Type | Description |
| --- | --- | --- |
| `cardId`required | `integer` |   |
| `noteId` | `integer` |   |
| `notetypeId` | `integer` |   |
| `deckId` | `integer` |   |
| `ord` | `integer` |   |
| `isEmpty` | `boolean` |   |
| `math`required | `boolean` | The question or the answer holds a formula (`\(...\)` or `\[...\]`) for MathJax to typeset. |
| `question`required | `string` |   |
| `answer`required | `string` |   |
| `css`required | `string` |   |
| `media`required | `string[]` |   |

- 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}/scheduling`read

### 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**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The 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**

| Name | Type | Description |
| --- | --- | --- |
| `cardId`required | `integer` |   |
| `fsrs`required | `boolean` |   |
| `today` | `integer` |   |
| `nextDayAt` | `integer` |   |
| `buttons`required | `object[]` |   |

- 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}/answer`write

### 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**

| Name | Type | Description |
| --- | --- | --- |
| `id`required | `path · integer` | The numeric id, as returned by the list endpoint or when it was created. |
| `Idempotency-Key` | `header · string` | Any unique string, such as a UUID. Send the same key when you retry a write: the first answer is replayed (marked `X-Idempotent-Replay: true`) instead of doing the work twice. Keys are remembered for 24 hours. Reusing a key with a different request is `422 idempotency_conflict`. |

Request body

**Answer (review) a card: request body**

| Name | Type | Description |
| --- | --- | --- |
| `rating`required | `object` |   |
| `timeTakenMs` | `integer` |   |

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**

| Name | Type | Description |
| --- | --- | --- |
| `revlogId`required | `integer` | Id of the review-log entry, a millisecond timestamp. |
| `leeched`required | `boolean` | `true` when this answer made the card a leech. |
| `buriedSiblingIds`required | `integer[]` | Other cards of the same note that were buried until tomorrow. |
| `card`required | `Card` | The 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/queue`read

### 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**

| Name | Type | Description |
| --- | --- | --- |
| `deckId` | `query · integer` | Only items that belong to this deck. |
| `limit` | `query · integer` | Page 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**

| Name | Type | Description |
| --- | --- | --- |
| `deckId`required | `integer` |   |
| `counts`required | `object` |   |
| `total`required | `integer` |   |
| `learnAheadSecs`required | `integer` |   |
| `cards`required | `object[]` |   |

- 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/stats`read

### 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**

| Name | Type | Description |
| --- | --- | --- |
| `q` | `query · string` | A search, written in Anki's search syntax: the text you would type in the browser, such as `deck:Languages is:due -tag:easy`. Leave it out for everything. The terms, the differences from desktop Anki, `fz:` for typos and the 1000-character limit are in [Searching](https://ankinize.com/docs/api#search); a search Anki would refuse is `400 invalid_input`. |
| `days` | `query · integer` | Days 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**

| Name | Type | Description |
| --- | --- | --- |
| `buttons`required | `object` | How 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. |
| `cardCounts`required | `object` | Cards 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. |
| `hours`required | `object` | Answers 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. |
| `today`required | `object` | What 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). |
| `eases`required | `object` | SM-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`. |
| `difficulty`required | `object` | FSRS cards: how many fall in each 1% bin of difficulty (0 to 99), and the median difficulty in percent. |
| `intervals`required | `object` | Review and relearning cards by interval, in days. |
| `stability`required | `object` | FSRS cards by stability, in days, rounded. |
| `futureDue`required | `object` | Cards 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. |
| `added`required | `object` | Cards by the day they were created (0 is today, -1 yesterday). |
| `reviews`required | `object` | Answers (`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. |
| `rolloverHour`required | `integer` | The hour of the day a new day starts (the `rollover` setting). |
| `retrievability`required | `object` | FSRS 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). |
| `fsrs`required | `boolean` | Whether the collection has FSRS turned on. |
| `trueRetention`required | `object` | Of 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. |
| `cards`required | `integer` | The number of cards the search matched. |
| `reviewEntries`required | `integer` | The 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). |
| `timing`required | `object` | The 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/media`read

### 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**

| Name | Type | Description |
| --- | --- | --- |
| `prefix` | `query · string` |   |
| `limit` | `query · integer` | Page size, from 1 to 100. Defaults to 50. |
| `cursor` | `query · string` | The 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**

| Name | Type | Description |
| --- | --- | --- |
| `items`required | `MediaFile[]` |   |
| `nextCursor`required | `string \| 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**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `path · string` | The tag's name, percent-encoded: `Languages%3A%3AGerman` is `Languages::German`. |
| `Range` | `header · string` |   |
| `If-None-Match` | `header · string` |   |
| `u` | `query · string` |   |
| `exp` | `query · integer` |   |
| `sig` | `query · 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**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `path · string` | The tag's name, percent-encoded: `Languages%3A%3AGerman` is `Languages::German`. |
| `u` | `query · string` |   |
| `exp` | `query · integer` |   |
| `sig` | `query · 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**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `path · string` | The 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**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `string` |   |
| `size`required | `integer` |   |
| `sha1`required | `string` |   |
| `sha256` | `string \| null` |   |
| `contentType`required | `string` |   |
| `mtime`required | `integer` |   |
| `usn`required | `integer` |   |
| `created`required | `boolean` |   |
| `unchanged`required | `boolean` |   |

- 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**

| Name | Type | Description |
| --- | --- | --- |
| `name`required | `path · string` | The 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**

| Name | Type | Description |
| --- | --- | --- |
| `ok`required | `boolean` |   |
| `name`required | `string` |   |
| `usn`required | `integer` |   |

- 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.
