AnkiWeb API — full /svc/* compatibility + add-on tools
A complete Python client for AnkiWeb's reverse-engineered /svc/* API —
all 41 public endpoints as typed AnkiWebClient methods — plus tools to
inspect, download, and upload Anki add-ons from
your terminal or GitHub Actions.
Fork of danny900714/upload-anki-addon (
upstream/mainremote). The Node upload engine (dist/index.js) is unmodified upstream code. This repository adds terminal tooling and a full schema-driven API client decoded from AnkiWeb's own frontend bundle.
Features
- Full API coverage: All 41 public
/svc/*endpoints as typed methods (reference) — account, decks, editor, search, shared, study — decoded from AnkiWeb's own frontend bundle, verified live. - Multiple branches: Upload different add-on files targeting different Anki version ranges.
- Description file: Use a separate file for the add-on description, making it easier to edit and format.
- Terminal CLI: Upload, inspect, download, and call any endpoint (
ankicli.py call ...) without GitHub Actions. - Metadata-only updates: Change title/description/tags without re-uploading the
.ankiaddon.
Table of contents
- Python API client — quick start · endpoint reference
- AnkiWeb API docs · full endpoint reference
- Terminal usage — setup · upload · info · download · notes
- List your own shared items
- Tests
- GitHub Actions usage — inputs · outputs
Python API client (full /svc/* coverage)
All 41 public endpoints are typed methods on AnkiWebClient — account (12),
decks (8), editor (3), search (1), shared (12), and study (4). The wire schema
is decoded from AnkiWeb's own frontend bundle (revision 2026-10-10, all
request/response/nested messages resolved) and verified live. Full field-level
reference: docs/ENDPOINTS.md.
Quick start
Requires Python 3.9+ with requests (same install as the terminal tools):
from ankiweb_client import AnkiWebClient
client = AnkiWebClient() # auto-loads .env (ANKIWEB_USERNAME/PASSWORD)
client.login()
mine = client.shared_list_mine() # your shared add-ons & decks
for item in mine["items"]:
print(item["id"], item["title"])
# Public, no auth needed:
item = client.shared_item_info(shared_id=2055492159)
print(item["available"]["title"])
# The rest of the surface:
client.decks_create_deck("My Deck")
client.search_search("deck:My Deck")
info = client.decks_deck_list_info(minutes_west_of_utc=0)
Method names mirror the path (/svc/shared/list-mine → shared_list_mine).
Legacy aliases login, list_mine, and item_info still work.
Any endpoint from the terminal
ankicli.py can call any public endpoint and print decoded JSON:
./ankicli.py endpoints # list all 41 with request/response types
./ankicli.py call shared_item_info --query sharedId=2055492159
./ankicli.py call shared_list_addons # public, no auth
./ankicli.py call decks_create_deck --request '{"name": "New Deck"}' # uses .env
Keeping the schema fresh
AnkiWeb ships no official API, so the schema can change. Regenerate and review the diff whenever something breaks:
python3 tools/generate_schema.py # re-decode the live frontend bundle
Details: docs/API.md (client usage, regeneration, codec) and docs/ENDPOINTS.md (every endpoint, field, and enum).
Terminal usage
Requires Python 3.9+ with requests, plus Node 24 if you upload (the action engine runs on Node).
info and download are Python-only and need no Node.
Setup
Install as a command:
uv tool install ankiweb-api # provides: ankiweb, ankiweb-upload, ankiweb-test-addon
Or run it straight from a checkout:
git clone https://github.com/athulkrishna2015/ankiweb-api.git
cd ankiweb-api
uv run --with requests ./ankicli.py info 805854302
Keep credentials in .env (gitignored — never commit it). The tools look for .env in
the current directory first, then next to the script:
cp .env.example .env
$EDITOR .env # ANKIWEB_USERNAME=... / ANKIWEB_PASSWORD=...
upload.py reads ./.env automatically. Exported shell variables take precedence, and
--username / --password override both. When installed as a tool, point the engine at
your own Node build with ANKIWEB_ENGINE=/path/to/dist/index.js.
Every example below uses the in-checkout form; the installed commands are equivalent:
| Script | Installed command |
|---|---|
./ankicli.py … |
ankiweb … |
./upload.py … |
ankiweb-upload … |
./make_test_addon.py |
ankiweb-test-addon |
Releasing to PyPI
Publishing uses trusted publishing (OIDC) — there is no API token stored anywhere.
.github/workflows/publish.yml builds on a tag and uploads with a short-lived token
that PyPI exchanges for an upload token.
One-time setup, once the project exists:
-
Create the project
ankiweb-apion PyPI (underathulkrishna2015). -
On that project: Manage → Publishing → Add a pending publisher:
Field Value PyPI project name ankiweb-apiOwner athulkrishna2015Repository name ankiweb-apiWorkflow name publish.yml -
Publish a release —
workflow_dispatchon that workflow, or tag a release, and it builds and uploads. The first successful upload activates the pending publisher; the name is then permanently claimed.
Upload
Same inputs as the action, as CLI flags. upload.py maps them to the action's INPUT_*
variables and runs the upstream engine — no separate upload implementation.
# New add-on (omit --addon-id)
./upload.py --title "My Anki add-on" \
--description "Description for my add-on" \
--branches "2.1.1-25.09.4+" \
--addon-files my-addon.ankiaddon
# Update an existing add-on, with description from a file
./upload.py --addon-id 805854302 --title "My Anki add-on" \
--description-file DESCRIPTION.md \
--branches "2.1.1-25.09.4+" \
--addon-files my-addon.ankiaddon
Metadata-only — update title/description/tags with no file upload. Use - for the file,
the same convention the action uses:
./upload.py --addon-id 805854302 --title "My Anki add-on" \
--description "Updated description" \
--branches "2.1.1-2.1.1+" \
--addon-files -
Useful flags:
| Flag | Description |
|---|---|
--tags "a b" |
Space-separated tags. |
--support-page URL |
Must start with http. |
--dry-run |
Print the INPUT_* mapping and exit without network access. |
--env-file PATH |
Use a different .env (default ./.env). |
--node PATH |
Node binary to run the engine with (default node). |
Multi-branch works the same way — one branch and one file per line, in matching order:
./upload.py --addon-id 805854302 --title "My Anki add-on" --description "Desc" \
--branches "2.1.1-2.1.66" "23.10-25.09.4+" \
--addon-files first.ankiaddon second.ankiaddon
List your own shared items
Everything on your account (add-ons and decks), with download counts and votes.
Uses the credentials from .env.
./ankicli.py mine
./ankicli.py mine --json
22 shared item(s), 46 review(s):
ID UPD DL + - TITLE
------------------------------------------------
2119980872 2026-10-10 856 3 1 AI-Hints and Options
226796325 2026-09-25 2080 5 2 Background Auto Sync
196373966 2026-07-17 4245 7 1 csv import plus
Pair it with info for full metadata on any id from the list:
for id in $(./ankicli.py mine --json | jq -r '.[].id'); do ./ankicli.py info "$id"; done
Info (add-on metadata)
Public, no auth. Reads title, tags, support page, size, versions, and description
from AnkiWeb's svc/shared/item-info protobuf endpoint.
./ankicli.py info 805854302
id: 805854302
title: Integration test for ankiweb client of @athulkrishna2015/ankiweb-api
tags: integration test tag
support: https://github.com/danny900714/upload-anki-addon
size: 172
last_updated: 1791704155 (2026-10-11T07:35:55+00:00)
versions: 2.1.1-25.09.4+
description:
<p>Throwaway fixture for upload integration tests. Limit probe: 65535 bytes OK, 65536 rejected.</p>
verify: https://ankiweb.net/shared/info/805854302?cb=1791710745423
Download .ankiaddon
Public, no auth. Follows the /shared/download/<id> redirect and writes the file.
./ankicli.py download 805854302
./ankicli.py download 805854302 -o /tmp/test.ankiaddon
./ankicli.py download 805854302 --point-version 250902 # Anki int_version
saved: Integration_test_for_ankiweb_client_of_athulkrishna2015ankiweb-api.ankiaddon
filename: Integration_test_for_ankiweb_client_of_athulkrishna2015ankiweb-api.ankiaddon
size: 172
t=1791704155 minpt=1 maxpt=250904 bidx=0
Equivalent one-liner, no Python needed:
curl -sL "https://ankiweb.net/shared/download/805854302?v=2.1&p=250902" -o addon.ankiaddon
Terminal notes
- Description size limit: 65,535 bytes. Measured on 2026-10-11 by probing the throwaway add-on — 65,535 stores, 65,536 is rejected. An over-limit upload is rejected, not truncated: the add-on keeps its previous description, so verify after every large upload.
infoprints averify:line with a cache-busted link. AnkiWeb servesitem-infowithmax-age=600, so a plain/shared/info/<id>URL can show up to 10 minutes of stale data; thecbquery is required to see what was just uploaded.v=2.1is required on the download URL; without it AnkiWeb replies 404 ("Your version of Anki is too old").pis Anki'sint_version().- The
.ankiaddonmust be a zip with__init__.pyat the root — no wrapping folder, no__pycache__(see Anki's sharing docs).make_test_addon.pybuilds a compliant fixture you can inspect. - Never commit
.envor AnkiWeb credentials.
Tests
python3 test_terminal_ankiweb.py # offline, no network
python3 test_terminal_ankiweb.py --live # + live AnkiWeb round-trips
python3 -m unittest -v test_ankiweb_proto.py # API codec and schema regressions
Offline tests parse real captured responses in fixtures/ (private add-on and
AnkiConnect, which carries 133 KB of reviews) and cover CLI exit codes.
Live tests hit info / download / error paths. API client and codec
documentation is in docs/API.md; the complete field-level
reference for all 41 public endpoints is docs/ENDPOINTS.md.
Live upload verification (GitHub Actions engine)
Verified live on 2026-10-11 against the private throwaway add-on
805854302 — saved as
ANKIWEB_TEST_ADDON_ID in the gitignored .env (never committed), used by
local runs and the CI test-action / integration jobs (vars.ADDON_ID,
vars.INTEGRATION_TEST_ADDON_ID):
python3 make_test_addon.py -o /tmp/gh-action-test.ankiaddon
./upload.py --addon-id "$ANKIWEB_TEST_ADDON_ID" --title "testingaddon for me [private]" \
--description "test upload via GitHub-Actions engine - 2026-10-11T07:28:56Z" \
--branches "2.1.1-25.09.4+" --addon-files /tmp/gh-action-test.ankiaddon
./ankicli.py info "$ANKIWEB_TEST_ADDON_ID" # new description + timestamp confirmed
./ankicli.py download "$ANKIWEB_TEST_ADDON_ID" -o /tmp/verify.ankiaddon # sha256 matches upload
upload.py execs node dist/index.js — the exact engine the
test-action CI job runs — so this covers the GitHub Actions upload path.
Downloaded bytes were sha256-identical to the uploaded fixture.
Same upload verified from real runners: CI run 38121891143 (2026-10-11) is
fully green, with the test-action job uploading from ubuntu-latest:
Successfully login to AnkiWeb.
Find addon file in test-addon-1.ankiaddon. Uploading it to 2.1.1-25.09.4+ branch ...
Successfully uploaded addon file: test-addon-1.ankiaddon.
Note: AnkiWeb requires site-wide unique add-on titles, so the fork's CI test
titles are qualified with @athulkrishna2015/ankiweb-api — the inherited
@danny900714 titles are already taken and fail with 400 title must be unique.
GitHub Actions usage
The action itself is unchanged from upstream. Use this fork's reference if you want the
terminal additions from CI; otherwise danny900714/upload-anki-addon@v1 behaves identically.
Upload a new add-on to AnkiWeb
If addon-id is not provided, a new add-on will be created on AnkiWeb.
Specify the Anki versions your add-on supports using the branches parameter.
Use the + symbol for the max version to indicate that your add-on supports all future versions as well.
name: Release
on:
release:
types: [published]
permissions:
contents: read
jobs:
upload-addon:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Package add-on
run: cd src/my-addon && zip -r ../../my-addon.ankiaddon *
- name: Upload add-on to AnkiWeb
uses: danny900714/upload-anki-addon@v1
with:
username: ${{ vars.ANKIWEB_USERNAME }}
password: ${{ secrets.ANKIWEB_PASSWORD }}
title: My Anki add-on
description: Description for my Anki add-on
branches: 2.1.1-25.09.4+
addon-files: my-addon.ankiaddon
Using a description file
You can use a separate file for the add-on description, which makes it easier to edit and format.
Note that description takes precedence over description-file if both are provided.
name: Release
on:
release:
types: [published]
permissions:
contents: read
jobs:
upload-addon:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Package add-on
run: cd src/my-addon && zip -r ../../my-addon.ankiaddon *
- name: Upload add-on to AnkiWeb
uses: danny900714/upload-anki-addon@v1
with:
username: ${{ vars.ANKIWEB_USERNAME }}
password: ${{ secrets.ANKIWEB_PASSWORD }}
addon-id: 12345678
title: My Anki add-on
description-file: README.md
branches: 2.1.1-25.09.4+
addon-files: my-addon.ankiaddon
Multiple branches
You can target multiple Anki version ranges by specifying multiple branches, one per line.
The number of addon-files must match the number of branches.
name: Release
on:
release:
types: [published]
permissions:
contents: read
jobs:
upload-addon:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Package add-on for first branch
run: cd src/first-branch && zip -r ../../first-branch.ankiaddon *
- name: Package add-on for second branch
run: cd src/second-branch && zip -r ../../second-branch.ankiaddon *
- name: Upload add-on to AnkiWeb
uses: danny900714/upload-anki-addon@v1
with:
username: ${{ vars.ANKIWEB_USERNAME }}
password: ${{ secrets.ANKIWEB_PASSWORD }}
addon-id: 12345678
title: My Anki add-on
description: Description for my Anki add-on
branches: |
2.1.1-2.1.66
23.10-25.09.4+
addon-files: |
first-branch.ankiaddon
second-branch.ankiaddon
Customizing
Inputs
The following inputs can be used as step.with keys:
Listtype is a newline-delimited stringbranches: | 2.1.1-2.1.66 23.10-25.09.4+
| Name | Type | Required | Description |
|---|---|---|---|
username |
String | true | AnkiWeb username. |
password |
String | true | AnkiWeb password. |
addon-id |
String | false | The download code for the add-on. Omit this when uploading a new add-on. |
title |
String | true | The title of the add-on. Must be fewer than 80 characters. |
tags |
String | false | Space-separated tags for this add-on. |
support-page |
String | false | A bug tracker, forum link, or other page where users can reach you. Must start with http. |
branches |
List | true | A list of Anki version branches that your add-on supports. Each branch must be in the format min-max, where min and max are Anki versions. Use + for the max version to indicate support for all future versions. |
addon-files |
List | true | A list of add-on files to upload, in the same order as branches. Use - to skip updating the add-on file for a branch. |
description |
String | false | The description of the add-on. Markdown and basic HTML are supported. AnkiWeb rejects descriptions larger than 65,535 bytes — the upload is refused and the add-on keeps its previous description. |
description-file |
String | false | Path to a file containing the add-on description. Markdown and basic HTML are supported. Ignored if description is also provided. |
Terminal flag equivalents
upload.py takes the same values as CLI flags. Each input maps 1:1:
| Input | Terminal flag | Notes |
|---|---|---|
username |
--username |
Default $ANKIWEB_USERNAME / .env. |
password |
--password |
Default $ANKIWEB_PASSWORD / .env. |
addon-id |
--addon-id |
Omit to create a new add-on. |
title |
--title |
Required, fewer than 80 characters. |
tags |
--tags "a b" |
Space-separated. |
support-page |
--support-page |
Must start with http. |
branches |
--branches a b |
Repeat the flag once per branch. |
addon-files |
--addon-files a b |
- skips a branch's file. |
description |
--description |
Takes precedence over --description-file. |
description-file |
--description-file |
Read from disk. |
Outputs
The following outputs can be accessed via ${{ steps.<step-id>.outputs }}:
| Name | Type | Description |
|---|---|---|
addon-id |
String | The id for the uploaded Anki add-on. |
Metadata
Release files for ankiweb-api 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ankiweb_api-1.0.0.tar.gz | 463.4 kB | With provenance | Details for ankiweb_api-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ankiweb_api-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for ankiweb_api-1.0.0-py3-none-any.whl |
Total release size: 926.3 kB
Release files / ankiweb_api-1.0.0.tar.gz
| Download URL | ankiweb_api-1.0.0.tar.gz |
|---|---|
| Size | 463.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
acf8a8ec11c8d8ad7dbe49777011f5c081b2bc7ddff5bd4e6b779f9c62bae9a0
|
|
BLAKE2b-256 checksum How to use checksums |
e61ca09b9d5867a8fa5de8f8e53079f49fb497547d356dee1b25a326a173be84
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logRelease files / ankiweb_api-1.0.0-py3-none-any.whl
| Download URL | ankiweb_api-1.0.0-py3-none-any.whl |
|---|---|
| Size | 462.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b18a7437bacdd728ea95185cbb715c57dcc28093f4c7fba87bc66a4b8691e4ab
|
|
BLAKE2b-256 checksum How to use checksums |
0db6a45fc483eb631af332f5c4fc082c3de196f177c9fcb096b9468f718c7202
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logSecurity for ankiweb-api 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for ankiweb_api-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.