Integration test for ankiweb client of @athulkrishna2015/ankiweb-api

AnkiWeb addon 805854302

Serves as a throwaway fixture for upload integration tests, verifying that a 65,535-byte upload is accepted while 65,536 bytes are rejected.
AI-generated summary; may contain mistakes.

developer-toolsno-description

Open on AnkiWeb GitHub Find alternatives

AnkiWeb

Votes
0 (πŸ‘ 0 Β· πŸ‘Ž 0)
Updated
2026-10-11
Anki versions
25.09.4~
License
MIT (from the GitHub repository)
Virus scan
No ClamAV match A match does not prove malware. No match does not prove that the code is safe.
Languages
Python, JSON
Description language
en

Maintenance

active

  • Last update or commit: 2026-10-24 (8 days before the snapshot).
  • Test files: 4.
  • GitHub Actions workflows: 3.
  • Commits in the last year: 77.
  • Issues: 0 open, 0 closed.
  • Pull requests: 0 open, 0 merged.

Will it work on my Anki?

Version branches
Min AnkiMax AnkiUpdated
2.1.125.09.4+2026-10-11
History across monthly snapshots

Loading…

Similar addons

personal

Serves as a work-in-progress plugin used to test the add-on upload process, with no user-facing features described.

no-description

5 votes 0% approval πŸ‘ 0 Β· πŸ‘Ž 5 Anki 23.09~ Updated 2023-10-29
Audio Normalizer

Measures card audio and creates verified, volume-adjusted copies for consistent loudness, with batch, preview, automatic, and original-restoring options.

note-managementbeta

1 votes 21% approval πŸ‘ 1 Β· πŸ‘Ž 0 ⭐ 0 Anki 26.05~ Updated 2026-08-12
StrideConnect

Automatically updates subscribed Anki decks from a remote server, with manual or startup checks, one-click updates, version tracking, changelog display, and a simple subscription configuration dialog.

importsync-integration

2 votes 34% approval πŸ‘ 2 Β· πŸ‘Ž 0 Anki 2.1.1~ Updated 2026-01-06
CSV Import Plus

Imports CSV or pasted text into Anki with auto-detected delimiters and note types, searchable deck selection, quick imports, subdeck creation, tag handling, and session history browsing or deletion.

card-creationimporttaggingdeck-management

8 votes 53% approval πŸ‘ 7 Β· πŸ‘Ž 1 ⭐ 1 Anki 25.09.4~ Updated 2026-10-22

Description

Throwaway fixture for upload integration tests. Limit probe: 65535 bytes OK, 65536 rejected.

README

AnkiWeb API β€” full /svc/* compatibility + add-on tools

CI PyPI Python

A Python client for AnkiWeb's reverse-engineered /svc/* API β€” all 41 public protobuf endpoints plus two live-discovered binary endpoints β€” alongside references to the separate official Anki sync protocol and AnkiDroid APIs. Also includes 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 AnkiWeb /svc/* coverage: All 41 public protobuf endpoints as typed methods (reference) β€” account, decks, editor, search, shared, study β€” decoded from AnkiWeb's own frontend bundle, verified live.
  • Live-discovered file endpoints: shared_download_deck() downloads any public shared deck's .apkg and shared_media_preview() fetches its media previews β€” endpoints found by sweeping AnkiWeb's live bundle (details).
  • Other Anki APIs documented: official collection/media sync, AnkiDroid's on-device APIs, and a map of the Anki API ecosystem. These are separate from AnkiWeb's /svc/* API and are documented for orientation, not implemented here.
  • 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.
  • Installable: uv tool install ankiweb-api β€” see setup.

[!CAUTION] These call AnkiWeb's reverse-engineered, unofficial API. Read the safety notes before pointing it at anything real.

Table of contents

Documentation

Deeper references live in docs/ β€” covering this client and the other official Anki API surfaces discovered in the ankitects / ankidroid orgs:

Doc What it covers
docs/Home.md Wiki landing page and quick-start for the documentation set.
docs/API.md Python client reference: installation, authentication, all 48 client methods, errors, codec, upload verification, description limits and cache-aware testing.
docs/ENDPOINTS.md Field-level reference for all 41 /svc/* endpoints, the reference-only admin endpoints, and live-discovered binary endpoints.
docs/ECOSYSTEM.md Map of Anki API surfaces across the ankitects and ankidroid GitHub orgs; use its surface map and choosing guide to pick the right API.
docs/SYNC.md Official collection and media sync protocol plus self-hosted sync server: /sync/* endpoints, /msync/* endpoints, wire format, authentication, configuration and client setup.
docs/ANKIDROID.md AnkiDroid's on-device integration APIs: Instant-Add/ContentProvider, AddContentApi methods, intents and the AnkiDroidJS bridge.

The sync protocol and AnkiDroid APIs are reference documentation only; this repository's Python client and GitHub Action target AnkiWeb's website API.

docs/ is published to the repository wiki by .github/workflows/sync-wiki.yml whenever it changes, so https://github.com/athulkrishna2015/ankiweb-api/wiki mirrors these pages.

Python API client (AnkiWeb /svc/*)

All 41 public protobuf 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. Two additional binary endpoints, shared_download_deck() and shared_media_preview(), were discovered by sweeping AnkiWeb's live frontend and are documented in the live-discovered section. The sync and AnkiDroid docs describe separate protocols; they are not extra AnkiWeb /svc/* methods.

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 from PyPI:

uv tool install ankiweb-api         # provides: ankiweb, ankiweb-upload, ankiweb-test-addon
# or: pipx install ankiweb-api

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

[!IMPORTANT] If you build the wheel yourself, send the artifacts somewhere other than dist/ β€” that directory holds dist/index.js, the action's upload engine, and CI diffs it: uv build --out-dir /tmp/wheel.

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.

The ankiweb-api project and trusted publisher are already configured. For each release, bump version in pyproject.toml, then publish a GitHub release or manually run Actions β†’ Publish to PyPI. The workflow builds and publishes both distributions. PyPI does not allow re-uploading an existing version.

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.

[!WARNING] Never test uploads with a real add-on file. An upload replaces the file on the target add-on id, and the previous version is not recoverable from AnkiWeb. Build the synthetic fixture instead and use a throwaway add-on id:

python3 make_test_addon.py          # -> fixtures/test.ankiaddon
./upload.py --addon-id <test-id> --title "CLI Test" \
    --description "test" --branches "2.1.1-2.1.1+" \
    --addon-files fixtures/test.ankiaddon
# 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

[!IMPORTANT] Branch notation is min-max. A bare 2.1.1+ is rejected. Trailing + means "and all future versions". Reproduce an existing range by reading it first: minpt/maxpt from ./ankicli.py download <id> -o - output map to it.

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
support:      https://github.com/athulkrishna2015/ankiweb-api/
size:         737
last_updated: 1791710361 (2026-10-11T09:19:21+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=&lt;fresh epoch milliseconds&gt;

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:     737
t=1791710361 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. Full write-up: docs/API.md.
  • 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 + API discovery
python3 -m unittest -v test_ankiweb_proto.py  # API codec and schema regressions
pnpm test                                # GitHub Action unit tests (mocked)

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 additionally re-discovers AnkiWeb's live bundle across all frontend chunks and diffs its endpoints against the committed schema, so a new or removed endpoint fails loudly instead of drifting. It also checks the two binary file endpoints that are not represented in the protobuf schema. It also asserts that long descriptions survive a real round-trip and that every read carries a distinct cache-buster β€” see docs/API.md. Last verified run: 40 checks offline; 61 checks with --live (including the offline checks), plus 5 codec tests and 16 GitHub Action unit tests.

[!NOTE] Live assertions must read through the cache-buster. item-info is served with max-age=600, so a test hitting a stable URL can pass or fail against stale data β€” which is exactly how a parser bug came to look like a server-side wipe. Always go through fetch_item_info() / ankicli.py info, never a hand-built URL.

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

[!TIP] Package the add-on before uploading. zip -r ../../my-addon.ankiaddon * from inside the add-on folder keeps __init__.py at the zip root (no wrapping folder, no __pycache__), which is what AnkiWeb requires and what ./upload.py --addon-files expects locally.

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.

[!NOTE]

  • description takes precedence over description-file if both are provided.
  • The number of addon-files must match the number of branches.

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.