{"id":45106570,"url":"https://github.com/s-salamatov/ticktick-python-sdk","last_synced_at":"2026-02-19T22:02:08.163Z","repository":{"id":339272952,"uuid":"1161112812","full_name":"s-salamatov/ticktick-python-sdk","owner":"s-salamatov","description":"Reverse-engineered Python SDK for the TickTick web app API — tasks, projects, tags, filters, habits, and more","archived":false,"fork":false,"pushed_at":"2026-02-18T21:34:47.000Z","size":78,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-19T03:06:04.441Z","etag":null,"topics":["api","productivity","python","reverse-engineering","sdk","task-management","ticktick","todo"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/s-salamatov.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-18T18:44:18.000Z","updated_at":"2026-02-18T21:34:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/s-salamatov/ticktick-python-sdk","commit_stats":null,"previous_names":["s-salamatov/ticktick-python-sdk"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/s-salamatov/ticktick-python-sdk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s-salamatov%2Fticktick-python-sdk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s-salamatov%2Fticktick-python-sdk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s-salamatov%2Fticktick-python-sdk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s-salamatov%2Fticktick-python-sdk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/s-salamatov","download_url":"https://codeload.github.com/s-salamatov/ticktick-python-sdk/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s-salamatov%2Fticktick-python-sdk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29634685,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T18:02:07.722Z","status":"ssl_error","status_checked_at":"2026-02-19T18:01:46.144Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","productivity","python","reverse-engineering","sdk","task-management","ticktick","todo"],"created_at":"2026-02-19T22:01:24.176Z","updated_at":"2026-02-19T22:02:08.155Z","avatar_url":"https://github.com/s-salamatov.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TickTick Python SDK\n\n![status: unofficial](https://img.shields.io/badge/status-unofficial-orange)\n![python: 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)\n[![CI](https://github.com/s-salamatov/ticktick-python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/s-salamatov/ticktick-python-sdk/actions/workflows/ci.yml)\n\n\u003e **Disclaimer:** This is an unofficial, reverse-engineered SDK based on the TickTick web app API.\n\u003e It is not affiliated with, endorsed by, or supported by TickTick or its parent company.\n\u003e API endpoints may change without notice. Use at your own risk.\n\n## Installation\n\n```bash\npip install ticktick-sdk        # future PyPI release\n```\n\nUntil the package is published, install directly from source:\n\n```bash\npip install requests\n# Clone the repo and add the project root to your PYTHONPATH\n```\n\n## Quick Start\n\n```python\nfrom ticktick_sdk import TickTickClient\n\nclient = TickTickClient()\nclient.login(\"your@email.com\", \"your_password\")\n\n# List all tasks\ntasks = client.task.get_all()\nfor t in tasks:\n    print(f\"[{t.priority}] {t.title} — {t.project_id}\")\n```\n\n## Authentication\n\n### Email / Password Login\n\n```python\nclient = TickTickClient()\nresult = client.login(\"email@example.com\", \"password\")\n\n# With MFA enabled:\nmfa = client.check_mfa_setting()\nif mfa.get(\"mfaType\"):\n    client.verify_mfa(input(\"Enter MFA code: \"))\n```\n\n### Token-Based Auth (Browser Cookie)\n\nIf you already hold a session token (value of the `t` cookie from a logged-in browser session):\n\n```python\nclient = TickTickClient(token=\"your_session_token\")\n# or\nclient = TickTickClient()\nclient.set_token(\"your_session_token\")\n```\n\n## API Coverage\n\n### Tasks (`client.task`)\n\n```python\nfrom datetime import datetime\n\n# Create\ntask = client.task.create(\n    \"Buy groceries\",\n    project_id=\"inbox\",          # defaults to inbox when omitted\n    priority=3,                  # 0=none, 1=low, 3=medium, 5=high\n    tags=[\"errands\"],\n    due_date=datetime(2026, 3, 1),\n    is_all_day=True,\n    content=\"Markdown notes here\",\n    items=[                       # Subtasks / checklist items\n        {\"title\": \"Milk\"},\n        {\"title\": \"Bread\"},\n    ],\n)\n\n# Read\ntask = client.task.get(\"task_id\", \"project_id\")\nall_tasks = client.task.get_all()\nproject_tasks = client.task.get_by_project(\"project_id\")\ncompleted = client.task.get_completed(project_id=\"project_id\")\nall_completed = client.task.get_completed_in_all()\ntrash = client.task.get_trash()\n\n# Update\ntask.title = \"Buy groceries and snacks\"\ntask.priority = 5\nclient.task.update(task)\n\n# Partial update (fetch + merge + save)\nclient.task.update_fields(\"task_id\", \"project_id\", title=\"New title\", priority=1)\n\n# Complete / Uncomplete\nclient.task.complete(\"task_id\", \"project_id\")\nclient.task.uncomplete(\"task_id\", \"project_id\")\n\n# Delete — uses POST /api/v2/batch/task with {\"delete\": [...]}\nclient.task.delete(\"task_id\", \"project_id\")\n\n# Move to another list\nclient.task.move(\"task_id\", \"old_project_id\", \"new_project_id\")\n\n# Set parent for nested tasks\nclient.task.set_parent(\"child_task_id\", \"project_id\", \"parent_task_id\")\n\n# Batch operations (single request)\nclient.task.batch_create([task1_dict, task2_dict])\nclient.task.batch_update([task1_dict, task2_dict])\nclient.task.batch_delete([{\"taskId\": \"id1\", \"projectId\": \"pid1\"}])\n```\n\n### Subtasks (`client.task`)\n\nSubtasks are checklist items embedded within a parent task.\n\n```python\n# Add a subtask\nclient.task.add_subtask(\"task_id\", \"project_id\", \"Subtask title\")\n\n# Mark a subtask complete\nclient.task.complete_subtask(\"task_id\", \"project_id\", \"subtask_id\")\n\n# Remove a subtask\nclient.task.remove_subtask(\"task_id\", \"project_id\", \"subtask_id\")\n```\n\n### Projects / Lists (`client.project`)\n\n```python\n# Create\nproject = client.project.create(\n    \"Work Tasks\",\n    color=\"#FF5733\",\n    view_mode=\"kanban\",    # \"list\", \"kanban\", or \"timeline\"\n    kind=\"TASK\",           # \"TASK\" or \"NOTE\"\n    group_id=\"folder_id\",  # place inside a folder (optional)\n)\n\n# Read\nprojects = client.project.get_all()    # uses full sync (checkpoint=0)\nproject  = client.project.get(\"project_id\")\ngroups   = client.project.get_groups()\n\n# Update — PUT returns empty body; the SDK re-fetches the project automatically\nproject.name = \"Updated Name\"\nclient.project.update(project)\nclient.project.rename(\"project_id\", \"New Name\")\n\n# Delete / Archive\nclient.project.delete(\"project_id\")\nclient.project.archive(\"project_id\")\nclient.project.unarchive(\"project_id\")\n\n# Project Groups (Folders)\ngroup = client.project.create_group(\"My Folder\")\nclient.project.move_to_group(\"project_id\", \"group_id\")\nclient.project.update_group(group)\nclient.project.delete_group(\"group_id\")\n\n# Templates\ntemplates = client.project.get_templates()\n```\n\n### Columns / Sections (`client.column`)\n\nColumns represent Kanban sections within a project. Create and update use the\nsame `POST /api/v2/column` endpoint — the API upserts by column ID.\n\n```python\n# Read\nall_cols     = client.column.get_all()\nproj_cols    = client.column.get_by_project(\"project_id\")\n\n# Create\ncol = client.column.create(\"project_id\", \"In Progress\")\n\n# Rename / Update\nclient.column.rename(\"column_id\", \"project_id\", \"Done\")\ncol.name = \"Shipped\"\nclient.column.update(col)\n\n# Delete is NOT supported — see Known Limitations\n```\n\n### Tags (`client.tag`)\n\nTags are hierarchical: a sub-tag is stored as `\"parent/child\"`. Create and\nupdate go through `POST /api/v2/batch/tag`; simple-tag delete uses\n`DELETE /api/v2/tag/{name}`.\n\n```python\n# Read — uses full sync (checkpoint=0)\ntags     = client.tag.get_all()\ntag      = client.tag.get(\"work\")\nchildren = client.tag.get_children(\"work\")   # returns tags named \"work/...\"\n\n# Create\nclient.tag.create(\"work\", color=\"#FF0000\")\nclient.tag.create(\"work/urgent\")             # sub-tag via name\nclient.tag.create_subtag(\"work\", \"urgent\")   # sub-tag via helper\n\n# Update properties (color, sort order, etc.)\ntag.color = \"#00FF00\"\nclient.tag.update(tag)\n\n# Rename across all tasks\nclient.tag.rename(\"old_name\", \"new_name\")\n\n# Delete — simple tags: DELETE /api/v2/tag/{name}\n#          sub-tags (contain \"/\"): POST /api/v2/batch/tag {\"delete\": [...]}\nclient.tag.delete(\"work\")\nclient.tag.delete(\"work/urgent\")\n\n# Completed tasks filtered by tag\ncompleted = client.tag.get_completed_tasks([\"work\", \"urgent\"], limit=50)\n```\n\n### Filters / Smart Lists (`client.filter`)\n\nAll filter mutations (create, update, delete) use `POST /api/v2/batch/filter`.\n\n```python\nfrom ticktick_sdk.managers.filter import FilterManager\n\n# Read — uses full sync (checkpoint=0)\nfilters = client.filter.get_all()\nfilt    = client.filter.get(\"filter_id\")\n\n# Build a rule and create\nrule = FilterManager.build_rule(\n    project_ids=[\"project_id_1\"],\n    priority=[5, 3],          # high and medium\n    tag_names=[\"urgent\"],\n)\nclient.filter.create(\"High Priority Urgent\", rule, view_mode=\"kanban\")\n\n# Update\nfilt.name = \"Renamed Filter\"\nclient.filter.update(filt)\n\n# Delete\nclient.filter.delete(\"filter_id\")\n```\n\n### Habits (`client.habit`)\n\nHabit **reads** and **check-ins** work correctly with cookie auth.\nHabit **create / update / delete** (`POST /api/v2/habits`, `PUT`, `DELETE`)\nall return HTTP 405 when authenticating via the cookie-based reverse-engineered\nsession. Treat habits as **read-only** unless you have a proper OAuth token.\n\n```python\n# Read\nhabits   = client.habit.get_all()\nactive   = client.habit.get_active()\narchived = client.habit.get_archived()\nhabit    = client.habit.get(\"habit_id\")\n\n# Check-in (works with cookie auth)\nclient.habit.checkin(\"habit_id\")                      # today, status=checked\nclient.habit.checkin(\"habit_id\", stamp=\"20260301\")    # specific date (YYYYMMDD)\nclient.habit.checkin(\"habit_id\", value=30, status=2)  # numeric Real habit\n\n# Batch check-in\nclient.habit.batch_checkin([\n    {\"habitId\": \"id1\", \"checkinStamp\": \"20260301\", \"value\": 1, \"status\": 2},\n    {\"habitId\": \"id2\", \"checkinStamp\": \"20260301\", \"value\": 1, \"status\": 2},\n])\n\n# Query check-ins\n# Returns a flat list of HabitCheckin objects.\n# Raw API format: {\"checkins\": {habit_id: [checkin, ...]}}\ncheckins = client.habit.get_checkins([\"habit_id\"], after_stamp=\"20260101\")\n\n# Preferences (read-only)\nprefs = client.habit.get_preferences()\n```\n\n### Search (`client.search`)\n\n```python\n# Cloud search across all content\nresults = client.search.search(\"meeting notes\")\n\n# Convenience wrapper returning Task objects\ntasks = client.search.search_tasks(\"tennis\")\n\n# Client-side filtering over the batch sync snapshot\nhigh_priority  = client.search.filter_tasks(priority=5)\ntagged         = client.search.filter_tasks(tag=\"urgent\", project_id=\"proj_id\")\nwith_due_dates = client.search.filter_tasks(has_due_date=True)\n```\n\n### User \u0026 Preferences (`client.user`)\n\n```python\nprofile       = client.user.get_profile()\nstatus        = client.user.get_status()\nsettings      = client.user.get_settings()\nlimits        = client.user.get_limits()\nnotifications = client.user.get_unread_notifications()\ncalendar      = client.user.get_calendar_events()\n```\n\n### Batch Sync (`client.batch`)\n\nThe batch sync endpoint is the canonical way TickTick transfers data between\nclient and server. Several managers call `check(0)` (full sync) internally to\nguarantee complete data because delta sync may return `None` for unchanged\ncollections such as `projectProfiles`, `tags`, and `filters`.\n\n```python\n# Full sync — returns everything (tasks, projects, tags, filters, …)\ndata = client.batch.full_sync()\n# Keys: syncTaskBean, projectProfiles, projectGroups, tags, filters,\n#       checkPoint, inboxId, syncTaskOrderBean, remindChanges\n\n# Delta sync — only changes since the last checkpoint\nchanges = client.batch.delta_sync()\n\n# Manual checkpoint management\nprint(client.batch.checkpoint)\nclient.batch.checkpoint = 0\n```\n\n## Data Models\n\nAll API objects are Python dataclasses with `from_dict()` / `to_dict()` helpers.\n\n| Model | Key Fields |\n|-------|------------|\n| `Task` | id, project_id, title, content, priority, status, tags, items, due_date, start_date, repeat_flag |\n| `Subtask` | id, title, status, sort_order |\n| `Project` | id, name, color, view_mode, kind, group_id |\n| `ProjectGroup` | id, name, show_all |\n| `Tag` | name, label, color, parent (for sub-tags) |\n| `Filter` | id, name, rule, view_mode |\n| `Habit` | id, name, type, goal, unit, repeat_rule, status |\n| `HabitCheckin` | id, habit_id, value, checkin_stamp, status |\n| `Column` | id, project_id, name, sort_order |\n\n### Priority Values\n\n| Value | Meaning |\n|-------|---------|\n| 0 | None |\n| 1 | Low |\n| 3 | Medium |\n| 5 | High |\n\n### Task / Subtask Status Values\n\n| Value | Meaning |\n|-------|---------|\n| 0 | Open |\n| 2 | Completed |\n\n## API Endpoints\n\nBase URL: `https://api.ticktick.com`\n\nOnly endpoints verified to work with cookie-based auth are listed.\n\n### Authentication\n| Method | Endpoint | Notes |\n|--------|----------|-------|\n| `POST` | `/api/v2/user/signon` | Sign in, returns token |\n| `GET` | `/api/v2/user/sign/mfa/setting` | Check MFA requirement |\n| `POST` | `/api/v2/user/sign/mfa/code/verify` | Verify MFA code |\n\n### Batch Sync\n| Method | Endpoint | Notes |\n|--------|----------|-------|\n| `GET` | `/api/v3/batch/check/{checkpoint}` | Full or delta data sync |\n| `POST` | `/api/v2/batch/task` | Task batch ops: `{add/update/delete: [...]}` |\n| `POST` | `/api/v2/batch/taskParent` | Set parent-child task relationships |\n| `POST` | `/api/v2/batch/tag` | Tag batch ops: `{add/update/delete: [...]}` |\n| `POST` | `/api/v2/batch/filter` | Filter batch ops: `{add/update/delete: [...]}` |\n\n### Tasks\n| Method | Endpoint | Notes |\n|--------|----------|-------|\n| `GET` | `/api/v2/task/{taskId}?projectId={id}` | Fetch single task |\n| `POST` | `/api/v2/task` | Create task |\n| `POST` | `/api/v2/task/{taskId}` | Update task |\n| `POST` | `/api/v2/batch/task` | Delete: `{\"delete\": [{\"taskId\",\"projectId\"}]}` |\n| `GET` | `/api/v2/project/{id}/completed/` | Completed tasks by project |\n| `GET` | `/api/v2/project/all/completed/` | All completed tasks |\n| `GET` | `/api/v2/project/all/completedInAll/` | Completed tasks (broad query) |\n| `GET` | `/api/v2/project/all/trash/pagination` | Trashed tasks |\n\n### Projects\n| Method | Endpoint | Notes |\n|--------|----------|-------|\n| `POST` | `/api/v2/project` | Create project |\n| `PUT` | `/api/v2/project/{id}` | Update project (returns empty body on success) |\n| `DELETE` | `/api/v2/project/{id}` | Delete project and all its tasks |\n| `POST` | `/api/v2/projectGroup` | Create project group (folder) |\n| `PUT` | `/api/v2/projectGroup/{id}` | Update project group |\n| `DELETE` | `/api/v2/projectGroup/{id}` | Delete project group |\n\n### Tags\n| Method | Endpoint | Notes |\n|--------|----------|-------|\n| `POST` | `/api/v2/batch/tag` | Create: `{\"add\": [tag_dict]}` |\n| `POST` | `/api/v2/batch/tag` | Update: `{\"update\": [tag_dict]}` |\n| `PUT` | `/api/v2/tag/rename` | Rename tag across all tasks |\n| `DELETE` | `/api/v2/tag/{name}` | Delete simple tag (no `/` in name) |\n| `POST` | `/api/v2/batch/tag` | Delete sub-tag: `{\"delete\": [\"parent/child\"]}` |\n| `POST` | `/api/v2/tag/completedTask` | Completed tasks filtered by tags |\n\n### Columns / Sections\n| Method | Endpoint | Notes |\n|--------|----------|-------|\n| `GET` | `/api/v2/column?from={ts}` | All columns (modified since timestamp) |\n| `GET` | `/api/v2/column/project/{id}` | Columns for a specific project |\n| `POST` | `/api/v2/column` | Create or update column (upsert by id) |\n\n### Habits (Read-Only)\n| Method | Endpoint | Notes |\n|--------|----------|-------|\n| `GET` | `/api/v2/habits` | Get all habits |\n| `POST` | `/api/v2/habitCheckins` | Record a single check-in |\n| `POST` | `/api/v2/habitCheckins/query` | Query check-ins; response: `{\"checkins\": {habit_id: [...]}}` |\n| `POST` | `/api/v2/habits/batch` | Batch check-ins |\n| `GET` | `/api/v2/user/preferences/habit` | Habit preferences |\n\n### Search\n| Method | Endpoint | Notes |\n|--------|----------|-------|\n| `GET` | `/api/v2/search/all?keywords={q}` | Cloud full-text search |\n\n### User\n| Method | Endpoint | Notes |\n|--------|----------|-------|\n| `GET` | `/api/v2/user/profile` | User profile |\n| `GET` | `/api/v2/user/status` | Account status and subscription |\n| `GET` | `/api/v2/user/preferences/settings` | User settings |\n| `POST` | `/api/v2/user/preferences/settings` | Update user settings |\n| `GET` | `/api/v2/configs/limits` | Account limits |\n| `GET` | `/api/v2/notification/unread` | Unread notifications |\n| `GET` | `/api/v2/calendar/third/accounts` | Linked third-party calendars |\n| `GET` | `/api/v2/calendar/subscription` | Calendar subscriptions |\n| `GET` | `/api/v2/calendar/bind/events/all` | All bound calendar events |\n\n## Known Limitations\n\n- **Habits are read-only.** `POST /api/v2/habits`, `PUT /api/v2/habits/{id}`,\n  and `DELETE /api/v2/habits/{id}` all return HTTP 405 when using cookie-based\n  authentication. Only GET and check-in endpoints work. Habit create/update/delete\n  methods are included in the SDK for future compatibility but will raise an\n  error at runtime.\n\n- **Column delete is not supported.** No standalone column delete endpoint has\n  been discovered. `client.column.delete()` raises `NotImplementedError`.\n  Deleting the parent project removes all its columns.\n\n- **Delta sync may omit unchanged data.** `GET /api/v3/batch/check/{cp}` with\n  a non-zero checkpoint can return `null` for `projectProfiles`, `tags`, and\n  `filters` when nothing has changed. Managers that need a complete list\n  (`project.get_all()`, `tag.get_all()`, `filter.get_all()`) always call\n  `check(0)` (full sync) to guarantee correctness.\n\n- **Rate limits.** TickTick may throttle requests. No official rate limit\n  documentation exists. The SDK does not implement automatic back-off beyond\n  basic error propagation.\n\n- **No official OAuth support.** This SDK authenticates via the same session\n  cookie the web app uses. There is no public OAuth 2.0 client ID available\n  for third-party use.\n\n## Architecture\n\n```\nticktick_sdk/\n    __init__.py          # Public exports and __version__\n    client.py            # TickTickClient: auth, HTTP layer, manager wiring\n    models.py            # Dataclasses: Task, Project, Tag, Filter, Habit, …\n    exceptions.py        # Typed HTTP error classes\n    managers/\n        task.py          # Task CRUD, subtasks, batch ops, completion\n        project.py       # Project/list CRUD, groups, archive, templates\n        tag.py           # Tag/sub-tag CRUD, tag-based queries\n        filter.py        # Saved filter CRUD with rule builder\n        habit.py         # Habit reads, check-ins (write ops return 405)\n        search.py        # Cloud search and client-side filtering\n        user.py          # Profile, preferences, notifications, calendar\n        batch.py         # Core batch sync (full and delta)\n        column.py        # Kanban columns / sections\n```\n\n## Contributing\n\nIssues and pull requests are welcome. Before opening a PR please:\n\n1. Verify the endpoint behaviour against a live TickTick session.\n2. Add or update the relevant manager and model.\n3. Update the endpoint table above to reflect the tested behaviour.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fs-salamatov%2Fticktick-python-sdk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fs-salamatov%2Fticktick-python-sdk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fs-salamatov%2Fticktick-python-sdk/lists"}