Backup your Any.do tasks to JSON and Markdown.
Flow inspired by Any.do's own efficient web implementation, ensuring this client remains respectful of Any.do's infrastructure while providing useful backup capabilities that do not currently exist on the official site.
This project is created as a tribute to Any.do's excellent task management service.
- 🛡️ Server-Friendly: Designed to minimize impact on Any.do's infrastructure with smart change detection and incremental sync
- 🔐 Secure Authentication: Session persistence with email 2FA support
- 📊 Multiple Export Formats: JSON and Markdown exports
- uv
- Any.do account
git clone <repository-url>
cd anydo-api
uv sync
uv run anydownOn first run, the script will prompt you to create a config.json with your Any.do credentials. You'll then receive a 2FA code by email to paste in.
Credentials can also be supplied via environment variables (ANYDO_EMAIL, ANYDO_PASSWORD) or the config file directly.
uv run anydown # Smart sync (incremental when possible)
uv run anydown --full-sync # Force full sync
uv run anydown --quiet # Reduce output
uv run anydown --debug # Verbose debug loggingKeep the process running and sync on a recurring schedule:
uv run anydown --watch # Sync every 90 ± 10 minutes (default)
uv run anydown --watch --watch-interval 60 # Change base interval to 60 minutes
uv run anydown --watch --watch-interval 60 --watch-jitter 5 # Tighter jitter
uv run anydown --watch --full-sync # Force full sync each timeThe jitter is re-randomised each sleep, so intervals vary naturally (e.g. 82, 97, 88 min) rather than firing at a fixed cadence. Press Ctrl+C to stop.
Note: The Docker setup uses the same
--watchmode by default (see Docker below).
uv run anydown-debug # Troubleshoot login issues
uv run anydown-dupes # Find duplicate tasks (dry run)
uv run anydown-dupes --delete # Fresh-sync, confirm, then delete via API
uv run anydown-dupes --delete --yes # Skip confirmation prompt
uv run anydown-dupes --keep newest # Keep newest copy instead of oldestRun continuously with jittered watch-mode sync (default: every 90 ± 10 minutes):
docker compose pull
docker compose up -dThis expects:
config.jsonin the repo root (mounted read-only)outputs/directory will be created for exports
Images are published to ghcr.io/aioue/any.down on each push to main (:latest) and on version tags (:v1.2.3). For local development you can still docker build -t anydown . and set image: anydown in docker-compose.yml.
The container runs anydown --watch as its main process. Adjust scheduling with ANYDOWN_WATCH_INTERVAL and ANYDOWN_WATCH_JITTER in docker-compose.yml. Session state is persisted in a Docker volume. Timezone is autodetected from the host via /etc/localtime.
When ANYDOWN_API_ENABLED=1 (default in Docker), a lightweight JSON API runs on port 8080 alongside watch mode:
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness check |
GET |
/agent or /api/agent |
Latest outputs/agent/latest.json payload |
GET |
/agent?live=1 |
Run sync first, then return agent JSON |
POST |
/sync or /api/sync |
Trigger sync and return agent JSON |
Optional bearer auth: set ANYDOWN_API_TOKEN and send Authorization: Bearer <token>.
curl -s http://localhost:8080/health
curl -s http://localhost:8080/agent | jq '.pending_tasks'
curl -X POST http://localhost:8080/syncSee AGENT_API_HANDOFF.md for homelab deployment details and agent integration notes.
To override the timezone sent to the Any.do API, set ANYDO_TIMEZONE in your environment or docker-compose.yml.
config.json (auto-created on first run, gitignored):
{
"email": "your@email.com",
"password": "your_password",
"save_raw_data": true,
"auto_export": true,
"text_wrap_width": 80,
"dedup_keep": "oldest",
"rotate_client_id": false
}dedup_keep controls which copy anydown-dupes --delete preserves: "oldest" (default) or "newest". Tasks are only considered duplicates when their title, list, parent task, note, and subtasks all match exactly.
rotate_client_id controls whether a new random device ID is generated on every run. The default (false) reuses the persisted ID so Any.do recognises the same device, which reduces unnecessary 2FA prompts.
If you hit login issues (2FA complications, rate limiting), you can extract a session cookie from your browser:
- Open Any.do in your browser, ensure you're logged in
- Open DevTools (F12) > Application > Cookies >
https://any.do - Copy the
SPRING_SECURITY_REMEMBER_ME_COOKIEvalue - Create
session.json(seesession.json.examplefor the template)
outputs/
├── agent/ # Compact JSON for agents (~70 KB)
│ └── latest.json # Stable path — pending tasks with IDs
├── raw-json/ # Complete API responses (~900 KB)
│ └── latest.json
└── markdown/ # Formatted task tables (~45 KB, no IDs)
└── latest.md
Files are timestamped (YYYY-MM-DD_HHMM-SS_*) and only created when data has actually changed (SHA-256 hash comparison). latest.* files are always refreshed on export.
uv sync # Install all deps (including dev)
uv run pytest -v # Run tests
uv run pytest -v --cov=anydown # With coverage
uv run ruff check . # Lint
uv run ruff check --fix . # Auto-fix lint
uv run ruff format . # FormatOr via Make:
make test
make lint
make formatanydo-api/
├── src/anydown/
│ ├── __init__.py
│ ├── client.py # API client library
│ ├── cli.py # Main CLI entry point
│ ├── debug_login.py # Login troubleshooting
│ └── find_duplicates.py # Duplicate finder & remover
├── tests/
├── pyproject.toml # Config, deps, tool settings
├── uv.lock # Locked dependency versions
├── Dockerfile
├── docker-compose.yml
└── entrypoint.sh
- Smart sync: Uses incremental sync to check if anything changed since the last run; only performs a full download when changes are detected
- Session persistence: Saves auth session to avoid repeated 2FA prompts
- Change detection: SHA-256 hashing of exported data prevents writing duplicate files
- Rate limiting: Client-side cooldown prevents full syncs more than once per minute
- Compression: Requests gzip/br/zstd; decompression handled by the HTTP library
- Retry logic: Exponential backoff with automatic retries on 429/5xx
Made with ❤️ for the Any.do community