Skip to content

ENDPOINTS

github-actions[bot] edited this page Oct 11, 2026 · 3 revisions

AnkiWeb API — full endpoint reference (41 public endpoints)

Status: full compatibility. Every non-admin /svc/* endpoint served by AnkiWeb's web frontend is decoded and callable via AnkiWebClient. Schema revision: 2026-10-10 — 174 messages, 41 public + 22 radmin endpoints. Regenerate any time with python3 tools/generate_schema.py.

AnkiWeb exposes no official public API. These endpoints are reverse-engineered from the site's own frontend bundle, may change without notice, and are subject to AnkiWeb's terms and rate limits. The 22 /svc/radmin/* admin endpoints are decoded into the schema for completeness but are not exposed on the client — see reference only. Use only with an account and data you are authorized to access.

Jump to: Account · Decks · Editor and search · Shared public reads · Shared account reads and writes · Study · Admin (reference only) · Discovered live · Wire details

Client concepts, safety rules, and schema regeneration are in API.md; installation and the CLI are in the README.

Related APIs: this reference covers /svc/* (AnkiWeb's website JSON API) only. Collection/media sync is a separate protocol served under /sync/* and /msync/* — see SYNC.md; AnkiDroid's on-device APIs are in ANKIDROID.md; ECOSYSTEM.md maps every surface.

Quick start

from ankiweb_client import AnkiWebClient

client = AnkiWebClient()   # reads .env (ANKIWEB_USERNAME / ANKIWEB_PASSWORD)
client.login()

mine = client.shared_list_mine()
print(mine["items"])

item = client.shared_item_info(shared_id=2055492159)  # no auth needed
print(item["available"]["title"])

Method names are <area>_<action>, e.g. account_login, shared_list_mine, decks_create_deck, study_study_cards. Legacy aliases login, list_mine, item_info still work.

Auth: POST endpoints attach the ankiweb session cookie automatically (login first), except the no-auth account endpoints (login, signup, reset-password, resend-verification, verify-email, get-account-status) and the three public GET endpoints (item-info, list-addons, list-decks).


Account (12)

Method Endpoint Auth Request Response
account_login(username, password) POST /svc/account/login no username, password {status, token} — status: UNKNOWN / AUTHENTICATED / INVALID_USER / INVALID_PASS
account_check_terms() POST /svc/account/check-terms yes — (empty) {needs_to_confirm: bool}
account_confirm_terms(agree_to_terms=True) POST /svc/account/confirm-terms yes agree_to_terms: bool — (empty)
account_get_account_status() POST /svc/account/get-account-status no — (empty) {logged_in: bool, redirect_to?: string}
account_get_settings() POST /svc/account/get-settings yes — (empty) {current_email: string}
account_update_settings(current_password, new_password, new_email) POST /svc/account/update-settings yes current_password, new_password, new_email {result} — UNKNOWN / PASSWORD_CHANGED / EMAIL_CHANGED / INVALID_CURRENT_PASSWORD / INVALID_EMAIL / EMAIL_UNAVAILABLE / SPAM_REPORT
account_signup(username, password) POST /svc/account/signup no username, password {status, ankiuser_login_token?} — UNKNOWN / SUCCESS / USERNAME_INVALID / USERNAME_TAKEN / SPAM_EMAIL
account_verify_email(token="") POST /svc/account/verify-email no token?: string {token_verified: bool, sending_status{updated, progress, email}} — progress: UNKNOWN / SENDING / DELIVERED / SPAM / QUOTA / OTHER_DELAY / FAILED
account_resend_verification(hkey="") POST /svc/account/resend-verification no hkey?: string {throttled: bool}
account_reset_password(email_to, check_token, new_password?) POST /svc/account/reset-password no email_to, check_token, new_password{token, new_password}? {send_email, token_ok: bool, set_new_password} — send_email: UNKNOWN / THROTTLED / INVALID_EMAIL / SUCCESS
account_total_media() POST /svc/account/total-media yes — (empty) {total_bytes: uint64}
account_remove_account(current_password) POST /svc/account/remove-account yes current_password {status, media_removed: uint32} — status: UNKNOWN / VALID_PASSWORD_REQUIRED / IN_PROGRESS / FINISHED. Irreversible.

Decks (8)

Method Endpoint Request Response
decks_create_deck(name) POST /svc/decks/create-deck name: string — (empty)
decks_deck_list_info(minutes_west_of_utc?) POST /svc/decks/deck-list-info minutes_west_of_utc?: int32 {top_node{deck_id, name, level, collapsed, review/learn/new_count, ...}, current_deck_id: int64, collection_size_bytes: uint32, media_size_bytes: uint64}
decks_deck_share_info(deck_id) POST /svc/decks/deck-share-info deck_id: int64 {metadata{title, tags, support_url, description, deck_id, shared_id?}, is_large_user: bool, share_count: uint32}
decks_deck_share(metadata, confirm_copyright=True) POST /svc/decks/deck-share metadata{...}, confirm_copyright: bool — (empty)
decks_deck_share_state() POST /svc/decks/deck-share-state — (empty) {state, shared_id?} — state: NO_ACTIVE_SHARE / WAITING / IN_PROGRESS / SUCCESS / TOO_LARGE / UNKNOWN_ERROR
decks_rename_deck(deck_id, new_name) POST /svc/decks/rename-deck deck_id: int64, new_name: string {error: string} (empty on success)
decks_remove_deck(deck_id) POST /svc/decks/remove-deck deck_id: int64 — (empty)
decks_select_deck(deck_id) POST /svc/decks/select-deck deck_id: int64 — (empty)

Editor and search (4)

Method Endpoint Request Response
editor_get_info_for_adding() POST /svc/editor/get-info-for-adding — (empty) {notetypes[], decks[], current_deck_id/notetype_id: int64, fields[]}
editor_get_note_info(note_id) POST /svc/editor/get-note-info note_id: int64 {fields[]: string, notetype_fields[], tags: string}
editor_get_notetype_fields(notetype_id) POST /svc/editor/get-notetype-fields notetype_id: int64 {fields[]}
editor_add_or_update(request) POST /svc/editor/add-or-update full EditorAddOrUpdateRequest (see SCHEMA) {...}
search_search(search) POST /svc/search/search search: string {notes[{...}], error: string}

Shared public reads (5, no auth)

Method Endpoint Request Response
shared_item_info(shared_id) GET /svc/shared/item-info?sharedId=<id> query sharedId: uint32 {available{title, description, tags, versions[], size, last_updated, reviews[]...}, missing: bool, access_denied: bool} — cached 600s server-side
shared_list_addons() GET /svc/shared/list-addons — {rows[{id, title, thumbs_up/down, mtime, max_supported_version}], user_upload_status} — UNSPECIFIED / NO_USER / TOO_NEW / CAN_UPLOAD
shared_list_decks(search="") GET /svc/shared/list-decks?search= — (but search required) {rows[{id, title, thumbs_up/down, mtime, notes, audio, images, ...}]}
shared_items_by_author(shared_id) POST /svc/shared/items-by-author shared_id: uint32 {items[{shared_id, title}]}
shared_get_addon_info(addon_id) POST /svc/shared/get-addon-info addon_id: uint32 {addon{addon_id?, title, tags, support_url, description, branches[{min_version, max_version}]}}

Shared account reads and writes (7, auth)

Method Endpoint Request Response
shared_list_mine() POST /svc/shared/list-mine — (empty) {items[{id, title, thumbs_up/down, mtime, downloads, max_supported_version?}], reviews[{review, item_id, hash}], expired_decks[]}
shared_remove_item(shared_id) POST /svc/shared/remove-item shared_id: uint32 — (empty)
shared_get_own_review(shared_id) POST /svc/shared/get-own-review shared_id: uint32 {review{body, thumbs_up}, shared_item_title, support_page?}
shared_set_own_review(shared_id, review) POST /svc/shared/set-own-review shared_id, review{body: string, thumbs_up: bool} — (empty)
shared_reply_to_review(shared_id, hash, body) POST /svc/shared/reply-to-review shared_Id (! exact wire name), hash: uint32, body: string — (empty)
shared_report_copyright(shared_id) POST /svc/shared/report-copyright shared_id: uint32 — (empty)
shared_upload_addon(info, file?) POST /svc/shared/upload-addon info{addon_id?, title, tags, support_url, description, branches[]}, file{zip_data: bytes, branch_index: uint32}? {addon_id: uint32} — also wrapped by upload.py / the GitHub Action

Study (4)

Method Endpoint Request Response
study_get_deck_limits() POST /svc/study/get-deck-limits — (empty) {new: uint32, review: uint32, new_cards_ignore_review_limit: bool, error: string}
study_set_deck_limits(new, review, new_cards_ignore_review_limit=False) POST /svc/study/set-deck-limits new, review: uint32, new_cards_ignore_review_limit: bool — (empty)
study_study_cards(answer?, next_card_id?) POST /svc/study/study-cards answer{card_id, answer_button, time_taken_millis, answered_at_millis, current/next_state}?, next_card_id?: int64 {sched_ver: uint32, cards[{card_id, question, answer, count_index, button_labels[], note_id, ...}], new/learn/review_count}
study_upgrade_scheduler(v3=True) POST /svc/study/upgrade-scheduler v3: bool — (empty)

Admin endpoints — reference only, not callable

Caution

Reference only. Not part of the supported surface. The 22 /svc/radmin/* endpoints below are decoded in SCHEMA — all request/response/nested names resolve — but are deliberately not exposed on AnkiWebClient and have no typed wrapper. They require AnkiWeb staff privileges and act on other users' accounts, so several are destructive and irreversible (ban-user, clear-reviews, bulk-remove-reviews, move-addon, restore-collection-backup, balance-users). They are listed so the decode coverage is honest and auditable, not as an invitation to call them.

They remain reachable through the codec if you deliberately construct a request (ankiweb_proto.encode("RadminBanUserRequest", {"user_id": 1})), because removing them from the schema would misreport what was actually decoded. Don't, unless you are AnkiWeb staff debugging a live incident.

Reference name (no wrapper exists) Endpoint Request fields Response fields
radmin_all_shared_items POST /svc/radmin/all-shared-items user_id items, reviews
radmin_balance_users POST /svc/radmin/balance-users batch_size, batch_count, shards_to_drain — (empty)
radmin_ban_user POST /svc/radmin/ban-user user_id — (empty)
radmin_become_user POST /svc/radmin/become-user user_id token
radmin_bulk_disable_sharing POST /svc/radmin/bulk-disable-sharing user_ids, reason — (empty)
radmin_bulk_remove_reviews POST /svc/radmin/bulk-remove-reviews user_ids, reason — (empty)
radmin_clear_reviews POST /svc/radmin/clear-reviews user_id, mode — (empty)
radmin_dump_all_addons POST /svc/radmin/dump-all-addons — — (empty)
radmin_email_status POST /svc/radmin/email-status email, action sent, changed, verified, state
radmin_export_revlogs POST /svc/radmin/export-revlogs shard, options — (empty)
radmin_fix_media POST /svc/radmin/fix-media user_id, dry_run files_processed
radmin_get_collection_backups POST /svc/radmin/get-collection-backups user_id backups
radmin_get_user_info POST /svc/radmin/get-user-info user_id user
radmin_list_media POST /svc/radmin/list-media user_id entries
radmin_lookup_users POST /svc/radmin/lookup-users search matched_users, first_match, ip_match
radmin_migrate_user POST /svc/radmin/migrate-user user_id, target_shard — (empty)
radmin_migrate_users POST /svc/radmin/migrate-users source_shard, target_shard, count — (empty)
radmin_move_addon POST /svc/radmin/move-addon shared_id, target_user_id — (empty)
radmin_recalculate_ratings POST /svc/radmin/recalculate-ratings shared_id — (empty)
radmin_restore_collection_backup POST /svc/radmin/restore-collection-backup user_id, version — (empty)
radmin_restore_media POST /svc/radmin/restore-media user_id, days, dry_run files_processed
radmin_support_notes POST /svc/radmin/support-notes user_id, new_note notes

Note

The first column is a documentation label, not a method name. There is no AnkiWebClient.radmin_ban_user() — calling it raises AttributeError. Only the 41 public endpoints in the sections above are callable.

The client enforces this in code, not just in docs: public_endpoints() filters /radmin/ out of the schema's endpoint list, and AnkiWebClient.__getattr__ refuses to synthesise a wrapper for any path that is admin-only.

Wire details

  • Transport: POST with Content-Type: application/octet-stream (protobuf bytes), except the three GET endpoints which pass URLSearchParams.
  • Session: Cookie: has_auth=1; ankiweb=<cookie> + Referer: https://ankiweb.net/shared/mine, exactly as the web app sends.
  • Verified live 2026-10-10: GET /svc/shared/item-info?sharedId=2055492159 → 200 → decodes as SharedItemInfoResponse via ankiweb_proto.decode.
  • Field numbers/types: see ankiweb_schema.SCHEMA (generated, authoritative as of the revision date). Regenerate with python3 tools/generate_schema.py and review the diff before accepting.

This is not the sync protocol

AnkiWeb serves two unrelated HTTP surfaces; only the first is documented here:

/svc/* (this page) /sync/* + /msync/*
Purpose Website features: add-ons, shared decks, study, account Collection & media sync between clients and a server
Encoding Protobuf messages in octet-stream bodies (codec: API.md) JSON (or multipart/gzip for legacy clients), zstd-compressed, anki-sync header envelope
Auth ankiweb session cookie (or none for public reads) Host key from POST /sync/hostKey ({u, p})
Official? No — reverse-engineered from the frontend bundle Yes — official protocol, client and server in ankitects/anki
Reference ENDPOINTS.md (here) SYNC.md

There is no overlap: an endpoint under /svc/* never appears under /sync/*, and a self-hosted sync server serves neither /svc/* nor any website features. In-app "AnkiWeb" messages during sync refer to the sync server, even against a custom one.

Discovered live, beyond the schema

Discovered 2026-10-11 by sweeping every chunk of AnkiWeb's live frontend bundle (the schema generator only reads the single frontend.*.mjs chunk) and probing the endpoints with a live session. These carry no protobuf message, so they can never appear in the generated schema — they are binary file endpoints wired up as plain links in the SvelteKit routes. All were verified live.

Endpoint Auth What it does Client
GET /svc/shared/download-deck/<sharedId>?t=<download_key> public Downloads a shared deck's .apkg file (zip). The one-time download_key is a short JWT ({"op":"sdd",...}) minted by item-info under available.deck.download_key. Verified: 200, Content-Disposition: ...apkg, zip magic PK\x03\x04, ~111 MB deck shared_download_deck()
GET /shared/mpreview/<sharedId>/<file>?<lastUpdated> public Media preview files for a shared deck (audio/images). The query key is the bare last_updated timestamp (...?1697739853=). Verified: 200 audio/mpeg for 0.mp3 shared_media_preview()
/svc/radmin/collection/<userId>/download admin only Direct collection download link on the radmin user page. 404 for non-admin sessions — (reference only)
/svc/radmin/get-collection-backup/<userId>/<backupId> admin only Single backup download (distinct from the protobuf get-collection-backups list). 404 for non-admin sessions — (reference only)
/svc/radmin/get-media/<userId>/<file> admin only Single media file download on the radmin page. 404 for non-admin sessions — (reference only)

Two live behaviours worth knowing (both discovered by probing):

  1. list-decks requires the search query field. The schema registers no query params, but the server rejects a bare call with 400 Failed to deserialize query string: missing field 'search'. An empty search returns an empty result; pass a term to list decks.
  2. Anonymous use is rate-limited. After a handful of anonymous calls the server answers 429 Please log in to perform more searches (list-decks, search) and 429 Please log in to download more decks (download-deck). The client therefore retries once with a session cookie when credentials are available (login_on_429=True), which lifts the limit.

The live test suite (python3 test_terminal_ankiweb.py --live) now sweeps all chunks and asserts this exact set: any new /svc/ path that appears in the bundle fails the no unknown /svc paths across all chunks check, and the download-deck / mpreview endpoints are exercised end-to-end.