ticktick-client
v0.3.0
Published
Unofficial TickTick API client for Node.js
Maintainers
Readme
ticktick-client
Unofficial TickTick API client for Node.js / TypeScript.
Disclaimer — This library reverse-engineers TickTick's private web API. It is not affiliated with or endorsed by TickTick. The API surface may change without notice. Use at your own risk.
Feature Coverage
The table below maps every major TickTick capability to its support status in this library. Each method has been verified against the real API via Playwright-based traffic capture (last verified: 2026-04-07).
| Category | Feature | Status | Method |
|----------|---------|:------:|--------|
| Tasks | List all tasks | :white_check_mark: | tasks.list() |
| | Create task | :white_check_mark: | tasks.create(draft) |
| | Update task | :white_check_mark: | tasks.update(params) |
| | Complete task | :white_check_mark: | tasks.complete(projectId, taskId) |
| | Delete task | :white_check_mark: | tasks.delete(projectId, taskId) |
| | Batch create | :white_check_mark: | tasks.createMany(drafts) |
| | Batch update | :white_check_mark: | tasks.updateMany(params) |
| | Batch delete | :white_check_mark: | tasks.deleteMany(items) |
| | Move to project | :warning: | tasks.move(item) — copy+delete, ID changes |
| | Move many | :warning: | tasks.moveMany(items) — same limitation |
| | Create subtask | :white_check_mark: | tasks.createSubtask(parentId, projectId, draft) |
| | Pin / Unpin | :white_check_mark: | tasks.pin() / tasks.unpin() |
| | List completed | :white_check_mark: | tasks.listCompleted(options) |
| | Iterate completed | :white_check_mark: | tasks.iterateCompleted(options) |
| | List trash | :no_entry_sign: | tasks.listTrash() — API ignores status filter |
| | Restore from trash | :warning: | tasks.restore() — works if you know the task ID |
| | Recurring tasks | :white_check_mark: | via repeatFlag / repeatEndDate in create/update |
| | Reminders | :x: | Not implemented |
| | Attachments | :x: | Not implemented |
| | Comments | :x: | Not implemented |
| | Sort order | :white_check_mark: | via sortOrder in create/update |
| Projects | List projects | :white_check_mark: | projects.list() |
| | Create project | :white_check_mark: | projects.create(draft) |
| | Update project | :white_check_mark: | projects.update(params) |
| | Delete project | :white_check_mark: | projects.delete(id) |
| | Batch delete | :white_check_mark: | projects.deleteMany(ids) |
| | List columns (Kanban) | :white_check_mark: | projects.listColumns(projectId) |
| | Sharing / Collaboration | :x: | Not implemented |
| Tags | List tags | :white_check_mark: | tags.list() |
| | Create tag | :white_check_mark: | tags.create(draft) |
| | Batch create | :white_check_mark: | tags.createMany(drafts) |
| | Update tag | :white_check_mark: | tags.update(draft) |
| | Delete tag | :white_check_mark: | tags.delete(name) |
| | Batch delete | :white_check_mark: | tags.deleteMany(names) |
| | Rename tag | :white_check_mark: | tags.rename(name, label) |
| | Merge tags | :white_check_mark: | tags.merge(source, target) |
| Habits | List habits | :white_check_mark: | habits.list() |
| | Create habit | :white_check_mark: | habits.create(draft) |
| | Update habit | :white_check_mark: | habits.update(params) |
| | Delete habit | :white_check_mark: | habits.delete(id) |
| | Batch delete | :white_check_mark: | habits.deleteMany(ids) |
| | Check in | :white_check_mark: | habits.upsertCheckin(input) |
| | Get check-ins | :white_check_mark: | habits.getCheckins(ids, start, end) |
| | Weekly stats | :white_check_mark: | habits.getWeekStats() |
| Focus | Start session | :white_check_mark: | focus.start(options) |
| | Pause session | :white_check_mark: | focus.pause() |
| | Resume session | :white_check_mark: | focus.resume() |
| | Finish session | :white_check_mark: | focus.finish() |
| | Stop (drop) session | :white_check_mark: | focus.stop() |
| | Get local state | :white_check_mark: | focus.getState() |
| | Sync remote state | :white_check_mark: | focus.syncState() |
| | Reset local state | :white_check_mark: | focus.resetState() |
| | Timeline | :white_check_mark: | focus.getTimeline(start, end) |
| | Overview | :white_check_mark: | focus.getOverview() |
| | Timing data | :white_check_mark: | focus.getTiming(start, end) |
| | Heatmap | :no_entry_sign: | focus.getHeatmap() — server returns 500 |
| | Hour distribution | :no_entry_sign: | focus.getHourDistribution() — server returns 500 |
| | Distribution | :no_entry_sign: | focus.getDistribution() — server returns 500 |
| Statistics | User ranking | :white_check_mark: | statistics.getRanking() |
| | Completed tasks list | :white_check_mark: | statistics.listCompleted(from, to, limit) |
| Countdowns | List countdowns | :white_check_mark: | countdowns.list() |
| | Create countdown | :white_check_mark: | countdowns.create(draft) |
| | Update countdown | :white_check_mark: | countdowns.update(params) |
| | Delete countdown | :white_check_mark: | countdowns.delete(id) |
| User | Get profile | :white_check_mark: | user.getProfile() |
| | Get status (Pro, etc.) | :white_check_mark: | user.getStatus() |
| Auth | Login | :white_check_mark: | client.login() |
| | Logout | :white_check_mark: | client.logout() |
| | Check auth | :white_check_mark: | client.isAuthenticated() |
| | Auto re-auth | :white_check_mark: | Automatic on 401/403 |
| | Session persistence | :white_check_mark: | File / Memory / Custom stores |
Legend: :white_check_mark: Fully working :warning: Works with known limitations :no_entry_sign: API broken server-side :x: Not implemented
MCP Server (Claude Integration)
This package includes a built-in MCP (Model Context Protocol) server that lets Claude Code and Claude Desktop interact with your TickTick account through natural language.
Setup
Claude Code
claude mcp add ticktick -e [email protected] -e TICKTICK_PASSWORD=your-password -- npx -y ticktick-clientClaude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"ticktick": {
"command": "npx",
"args": ["-y", "ticktick-client"],
"env": {
"TICKTICK_USERNAME": "[email protected]",
"TICKTICK_PASSWORD": "your-password"
}
}
}
}Available Tools (41)
| Module | Tools |
|--------|-------|
| Tasks | list_tasks, create_task, update_task, complete_task, delete_task, move_task, create_subtask, pin_task, unpin_task, list_completed_tasks |
| Projects | list_projects, create_project, update_project, delete_project, list_columns, list_project_members |
| Tags | list_tags, create_tag, update_tag, delete_tag, merge_tags |
| Habits | list_habits, create_habit, update_habit, delete_habit, checkin_habit, get_habit_week_stats |
| Focus | start_focus, pause_focus, resume_focus, finish_focus, stop_focus, get_focus_overview |
| Statistics | get_ranking, list_completed_in_range |
| User | get_user_profile, get_user_status |
| Countdowns | list_countdowns, create_countdown, update_countdown, delete_countdown |
Example Prompts
Once configured, just talk to Claude naturally:
- "What tasks do I have today?"
- "Create a task 'Review PR #37' in the Work project, due tomorrow, high priority"
- "Mark 'Buy groceries' as complete"
- "Start a 25-minute focus session"
- "How are my habits going this week?"
- "Show my productivity ranking"
Environment Variables
| Variable | Required | Description |
|----------|:--------:|-------------|
| TICKTICK_USERNAME | Yes | TickTick account email |
| TICKTICK_PASSWORD | Yes | TickTick account password |
| TICKTICK_SESSION_PATH | No | Session file path (default: ~/.ticktick-mcp-session.json) |
| TICKTICK_BASE_URL | No | API base URL (for Dida365: https://api.dida365.com) |
| TICKTICK_TIME_ZONE | No | Time zone override (default: system) |
Installation
npm install ticktick-clientRequires Node.js 22+. Zero runtime dependencies.
Quick Start
import { TickTickClient, FileSessionStore } from 'ticktick-client';
const client = new TickTickClient({
credentials: {
username: '[email protected]',
password: 'your-password',
},
// Persist session to avoid logging in every time
sessionStore: new FileSessionStore('./.ticktick-session.json'),
});
// First request triggers auto-login
const tasks = await client.tasks.list();
console.log(`You have ${tasks.length} tasks`);Authentication
Credentials (auto-login)
const client = new TickTickClient({
credentials: { username: '[email protected]', password: 'password' },
});
// Automatically logs in on first API call and re-authenticates on session expiry.Session Stores
| Store | Use case |
|-------|----------|
| FileSessionStore(path) | CLI tools, scripts — persists to disk |
| MemorySessionStore() | Short-lived processes, tests |
| Custom TickTickSessionStore | Implement load(), save(), delete() for any backend |
// File-based (recommended for scripts)
import { FileSessionStore } from 'ticktick-client';
const client = new TickTickClient({
credentials: { username: '...', password: '...' },
sessionStore: new FileSessionStore('./.ticktick-session.json'),
});
// Pre-loaded session (no credentials needed)
const client = new TickTickClient({
session: existingSessionObject,
});API Reference
Tasks
// List all active tasks
const tasks = await client.tasks.list();
// Create
const task = await client.tasks.create({
title: 'Buy groceries',
projectId: 'inbox123',
priority: 3, // 0=none, 1=low, 3=medium, 5=high
dueDate: '2026-12-31T00:00:00.000Z',
tags: ['shopping'],
});
// Update
await client.tasks.update({
id: task.id,
projectId: task.projectId,
title: 'Buy organic groceries',
priority: 5,
});
// Complete / Delete
await client.tasks.complete(task.projectId, task.id);
await client.tasks.delete(task.projectId, task.id);
// Batch operations
await client.tasks.createMany([
{ title: 'Task A', projectId },
{ title: 'Task B', projectId },
]);
await client.tasks.updateMany([
{ id: 'id1', projectId, priority: 5 },
{ id: 'id2', projectId, priority: 3 },
]);
await client.tasks.deleteMany([
{ taskId: 'id1', projectId },
{ taskId: 'id2', projectId },
]);Moving Tasks Between Projects
Important: The TickTick REST API does not support native task moves. This library uses a copy+delete strategy — the task ID will change. Use the returned
previousIdto update any references.
const result = await client.tasks.move({
taskId: 'old-id',
fromProjectId: 'project-a',
toProjectId: 'project-b',
});
console.log(result.previousId); // 'old-id'
console.log(result.task.id); // new server-assigned ID
console.log(result.task.projectId); // 'project-b'
// Batch move with ID mapping
const results = await client.tasks.moveMany([
{ taskId: 't1', fromProjectId: 'a', toProjectId: 'b' },
{ taskId: 't2', fromProjectId: 'a', toProjectId: 'b' },
]);
for (const r of results) {
console.log(`${r.previousId} -> ${r.task.id}`);
}Subtasks, Pinning, Recurring
// Subtask
await client.tasks.createSubtask(parentTask.id, projectId, {
title: 'Sub-item',
});
// Pin / Unpin
await client.tasks.pin(task.id, projectId);
await client.tasks.unpin(task.id, projectId);
// Recurring task
await client.tasks.create({
title: 'Weekly review',
projectId,
repeatFlag: 'RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=FR',
});Completed Tasks (Paginated)
// Single page
const completed = await client.tasks.listCompleted({ projectId, limit: 50 });
// Auto-paginated async iterator
for await (const page of client.tasks.iterateCompleted()) {
for (const task of page) {
console.log(task.title, task.completedTime);
}
}Projects
const projects = await client.projects.list();
const project = await client.projects.create({
name: 'Work',
color: '#ff6348',
kind: 'TASK', // 'TASK' | 'NOTE'
viewMode: 'kanban', // 'list' | 'kanban' | 'timeline'
});
await client.projects.update({ id: project.id, name: 'Work 2026' });
await client.projects.delete(project.id);
await client.projects.deleteMany([id1, id2]);
// Kanban columns
const columns = await client.projects.listColumns(project.id);Tags
const tags = await client.tags.list();
await client.tags.create({ name: 'urgent', label: 'urgent', color: '#ff0000' });
await client.tags.createMany([
{ name: 'work', label: 'work' },
{ name: 'personal', label: 'personal' },
]);
await client.tags.update({ name: 'work', color: '#0000ff' });
await client.tags.rename('work', 'office');
await client.tags.merge('office', 'personal'); // merge office into personal
await client.tags.delete('personal');
await client.tags.deleteMany(['tag1', 'tag2']);Habits
const habits = await client.habits.list();
await client.habits.create({
name: 'Exercise',
repeatRule: 'FREQ=DAILY',
goal: 1,
step: 1,
unit: 'times',
type: 'boolean',
recordEnable: false,
color: '#FF6B6B',
});
await client.habits.update({ id: habit.id, name: 'Morning Exercise' });
// Check in
await client.habits.upsertCheckin({
habitId: habit.id,
date: new Date(),
goal: 1,
value: 1,
status: 'done', // 'done' | 'undone' | 'unlabeled'
});
// Query check-ins for a date range
const checkins = await client.habits.getCheckins(
[habit.id],
'2026-04-01',
'2026-04-07',
);
// Weekly completion stats
const weekStats = await client.habits.getWeekStats();
await client.habits.delete(habit.id);
await client.habits.deleteMany([id1, id2]);Focus (Pomodoro)
// Start a focus session
await client.focus.start({
duration: 25, // minutes
focusOnTitle: 'Deep work',
focusOnId: taskId, // optional: link to a task
});
// Session lifecycle
await client.focus.pause();
await client.focus.resume();
await client.focus.finish(); // complete the pomodoro
await client.focus.stop(); // abandon (drop) the session
// Local state management (no network calls)
const state = client.focus.getState();
// { status: 'running' | 'paused' | 'idle' | null, focusId, duration, pomoCount, ... }
client.focus.resetState();
// Sync state from server
const remote = await client.focus.syncState();
// Analytics
const overview = await client.focus.getOverview();
// { todayPomoCount, todayPomoDuration, totalPomoCount, totalPomoDuration }
const timeline = await client.focus.getTimeline('2026-04-01', '2026-04-07');
// [{ id, startTime, endTime, status, pauseDuration, type }]
const timing = await client.focus.getTiming('2026-04-01', '2026-04-07');Statistics
const ranking = await client.statistics.getRanking();
// { ranking, taskCount, projectCount, dayCount, completedCount, score, level }
const completed = await client.statistics.listCompleted(
'2026-04-01 00:00:00',
'2026-04-07 23:59:59',
100, // limit
);Countdowns
const countdowns = await client.countdowns.list();
await client.countdowns.create({
name: 'Product Launch',
date: new Date('2026-12-31'),
type: 'countdown', // 'countdown' | 'anniversary' | 'birthday' | 'holiday'
color: '#ff6348',
});
await client.countdowns.update({ id: countdown.id, name: 'Big Launch Day' });
await client.countdowns.delete(countdown.id);User
const profile = await client.user.getProfile();
// { username, email, displayName, picture, locale, ... }
const status = await client.user.getStatus();
// { userId, username, pro, teamPro, proEndDate, inboxId, ... }Semantic Helpers
Utility functions for converting between human-readable labels and TickTick's numeric codes:
import {
parseTaskPriority, formatTaskPriority,
parseTaskStatus, formatTaskStatus,
parseHabitStatus, formatHabitStatus,
parseCheckinStatus, formatCheckinStatus,
} from 'ticktick-client';
parseTaskPriority('medium'); // 3
formatTaskPriority(5); // 'high'
parseTaskStatus('completed'); // 2
formatTaskStatus(0); // 'open'
parseHabitStatus('archived'); // 1
formatHabitStatus(0); // 'normal'
parseCheckinStatus('done'); // 2
formatCheckinStatus(1); // 'undone'Known Limitations
These are confirmed TickTick server-side issues, verified via Playwright network capture on 2026-04-07.
Task Move Changes ID (#32)
The REST API has no endpoint for moving tasks between projects. move() and moveMany() use a copy+delete strategy. The task receives a new ID. Use result.previousId to track the mapping.
Tested approaches that failed:
POST /api/v3/batch/taskProject→ 404POST /api/v2/task/{id}with newprojectId→ 200 but no actual change
Trash Listing Broken (#33)
listTrash() calls GET /api/v2/project/{id}/tasks?status=-1, but the status filter is ignored server-side. Deleted tasks are not retrievable via any known REST endpoint. restore() works if you already know the task ID.
Focus Analytics Endpoints Return 500 (#31)
getHeatmap(), getHourDistribution(), and getDistribution() always return HTTP 500 regardless of parameters or account data. All other focus endpoints (timeline, overview, timing, session control) work correctly.
Architecture
ticktick-client/
src/
client.ts # TickTickClient — auth, HTTP, session management
modules/
tasks.ts # TasksModule — CRUD, batch, move, subtasks, pin, trash
projects.ts # ProjectsModule — CRUD, columns
tags.ts # TagsModule — CRUD, rename, merge
habits.ts # HabitsModule — CRUD, check-ins, weekly stats
focus.ts # FocusModule — session control, analytics, state
statistics.ts # StatisticsModule — ranking, completed list
countdowns.ts # CountdownsModule — CRUD
user.ts # UserModule — profile, status
mcp/
index.ts # MCP server entry point (stdio transport)
server.ts # McpServer creation + LLM instructions
config.ts # Environment variable loading
client-factory.ts # Config → TickTickClient instance
error-handler.ts # Error mapping + stripUndefined utility
tools/ # 41 MCP tool definitions (one file per module)
types.ts # All TypeScript type definitions
errors.ts # TickTickError, TickTickAuthError, TickTickApiError
semantic.ts # Human-readable label converters
session-store.ts # FileSessionStore, MemorySessionStore
internal/
ids.ts # ObjectId generator
cookies.ts # Cookie parsing/serializationDevelopment
npm install # install dependencies
npm test # run unit tests (vitest)
npm run lint # type check (tsc --noEmit)
npm run build # build ESM + CJS + DTS (tsup)
# Integration test against real API (requires .ticktick-session.json)
npx tsx scripts/integration-test.ts
# Capture real API traffic via Playwright
npx tsx scripts/capture-all-issues.ts