npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ticktick-client

v0.3.0

Published

Unofficial TickTick API client for Node.js

Readme

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 | :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-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": "[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-client

Requires 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 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