Skip to main content

AnkiWeb API — full /svc/* compatibility + add-on tools

CI

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/main remote). 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 (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:

  1. Create the project ankiweb-api on PyPI (under athulkrishna2015).

  2. On that project: Manage → Publishing → Add a pending publisher:

    Field Value
    PyPI project name ankiweb-api
    Owner athulkrishna2015
    Repository name ankiweb-api
    Workflow name publish.yml
  3. Publish a release — workflow_dispatch on 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.
  • info prints a verify: line with a cache-busted link. AnkiWeb serves item-info with max-age=600, so a plain /shared/info/<id> URL can show up to 10 minutes of stale data; the cb query is required to see what was just uploaded.
  • v=2.1 is required on the download URL; without it AnkiWeb replies 404 ("Your version of Anki is too old"). p is Anki's int_version().
  • The .ankiaddon must be a zip with __init__.py at the root — no wrapping folder, no __pycache__ (see Anki's sharing docs). make_test_addon.py builds a compliant fixture you can inspect.
  • Never commit .env or 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:

List type is a newline-delimited string

branches: |
  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)

Source distribution for ankiweb-api 1.0.0
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)

Table of built distributions (wheels) for ankiweb-api 1.0.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

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

Select from 2 files

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 log

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

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page