Skip to content

jaeyeonling/ticktick-client

Repository files navigation

ticktick-client

npm version CI License: MIT

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 tasks.list()
Create task tasks.create(draft)
Update task tasks.update(params)
Complete task tasks.complete(projectId, taskId)
Delete task tasks.delete(projectId, taskId)
Batch create tasks.createMany(drafts)
Batch update tasks.updateMany(params)
Batch delete tasks.deleteMany(items)
Move to project ⚠️ tasks.move(item) — copy+delete, ID changes
Move many ⚠️ tasks.moveMany(items) — same limitation
Create subtask tasks.createSubtask(parentId, projectId, draft)
Pin / Unpin tasks.pin() / tasks.unpin()
List completed tasks.listCompleted(options)
Iterate completed tasks.iterateCompleted(options)
List trash 🚫 tasks.listTrash() — API ignores status filter
Restore from trash ⚠️ tasks.restore() — works if you know the task ID
Recurring tasks via repeatFlag / repeatEndDate in create/update
Reminders Not implemented
Attachments Not implemented
Comments Not implemented
Sort order via sortOrder in create/update
Projects List projects projects.list()
Create project projects.create(draft)
Update project projects.update(params)
Delete project projects.delete(id)
Batch delete projects.deleteMany(ids)
List columns (Kanban) projects.listColumns(projectId)
Sharing / Collaboration Not implemented
Tags List tags tags.list()
Create tag tags.create(draft)
Batch create tags.createMany(drafts)
Update tag tags.update(draft)
Delete tag tags.delete(name)
Batch delete tags.deleteMany(names)
Rename tag tags.rename(name, label)
Merge tags tags.merge(source, target)
Habits List habits habits.list()
Create habit habits.create(draft)
Update habit habits.update(params)
Delete habit habits.delete(id)
Batch delete habits.deleteMany(ids)
Check in habits.upsertCheckin(input)
Get check-ins habits.getCheckins(ids, start, end)
Weekly stats habits.getWeekStats()
Focus Start session focus.start(options)
Pause session focus.pause()
Resume session focus.resume()
Finish session focus.finish()
Stop (drop) session focus.stop()
Get local state focus.getState()
Sync remote state focus.syncState()
Reset local state focus.resetState()
Timeline focus.getTimeline(start, end)
Overview focus.getOverview()
Timing data focus.getTiming(start, end)
Heatmap 🚫 focus.getHeatmap() — server returns 500
Hour distribution 🚫 focus.getHourDistribution() — server returns 500
Distribution 🚫 focus.getDistribution() — server returns 500
Statistics User ranking statistics.getRanking()
Completed tasks list statistics.listCompleted(from, to, limit)
Countdowns List countdowns countdowns.list()
Create countdown countdowns.create(draft)
Update countdown countdowns.update(params)
Delete countdown countdowns.delete(id)
User Get profile user.getProfile()
Get status (Pro, etc.) user.getStatus()
Auth Login client.login()
Logout client.logout()
Check auth client.isAuthenticated()
Auto re-auth Automatic on 401/403
Session persistence File / Memory / Custom stores

Legend: ✅ Fully working   ⚠️ Works with known limitations   🚫 API broken server-side   ❌ 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 TICKTICK_USERNAME=you@example.com -e TICKTICK_PASSWORD=your-password -- npx -y ticktick-client

Claude 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": "you@example.com",
        "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-client

Requires Node.js 22+. Zero runtime dependencies.


Quick Start

import { TickTickClient, FileSessionStore } from 'ticktick-client';

const client = new TickTickClient({
  credentials: {
    username: 'your@email.com',
    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: 'you@example.com', 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 previousId to 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 → 404
  • POST /api/v2/task/{id} with new projectId → 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/serialization

Development

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

License

MIT

About

Unofficial TickTick API client for Node.js/TypeScript — zero dependencies, full TypeScript types, 50+ verified endpoints

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors