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

@panoptic-it-solutions/zoho-projects-client

v0.4.7

Published

TypeScript client for Zoho Projects V3 API with OAuth 2.0 and rate limiting

Readme

@panoptic-it-solutions/zoho-projects-client

TypeScript client for Zoho Projects V3 API with OAuth 2.0 and rate limiting.

Installation

npm install @panoptic-it-solutions/zoho-projects-client

AI Agent Setup (Claude Code)

For projects using Claude Code or other AI coding assistants, run the init command to set up helpful slash commands and documentation:

npx @panoptic-it-solutions/zoho-projects-client init

What it sets up

| File/Directory | Description | |----------------|-------------| | .claude/commands/zoho-projects.md | Full API reference with all namespaces and methods | | .claude/commands/zoho-auth.md | OAuth 2.0 setup guide | | .claude/commands/zoho-examples.md | Common usage patterns and examples | | CLAUDE.md | Project context file for AI assistants |

Available Slash Commands

After setup, use these commands in Claude Code:

  • /zoho-projects - Complete API reference
  • /zoho-auth - OAuth authentication setup guide
  • /zoho-examples - Code examples and patterns

Options

# Include type definitions for enhanced AI visibility
npx @panoptic-it-solutions/zoho-projects-client init --with-types

# Skip npm install (if already installed)
npx @panoptic-it-solutions/zoho-projects-client init --skip-install

# Show help
npx @panoptic-it-solutions/zoho-projects-client init --help

The --with-types option copies TypeScript definitions to .ai-types/ for AI assistants that benefit from having type information in the project.

Quick Start

import { createZohoProjectsClient } from "@panoptic-it-solutions/zoho-projects-client";

const client = createZohoProjectsClient({
  clientId: process.env.ZOHO_CLIENT_ID!,
  clientSecret: process.env.ZOHO_CLIENT_SECRET!,
  refreshToken: process.env.ZOHO_REFRESH_TOKEN!,
  portalId: process.env.ZOHO_PORTAL_ID!,
});

// List projects
const { data: projects } = await client.projects.list();

// Get all projects with auto-pagination
const allProjects = await client.projects.listAll();

// Iterate over projects
for await (const project of client.projects.iterate()) {
  console.log(project.name);
}

Authentication Setup

Zoho requires OAuth 2.0 with a refresh token. Here's how to get one:

1. Create a Client

  1. Go to Zoho API Console (or .eu, .in, .com.au for your region)
  2. Click Add ClientServer-based Applications
  3. Fill in the details and click Create
  4. Copy the Client ID and Client Secret

2. Generate a Refresh Token

Option A: Using the Helper Script (Recommended)

The easiest way to get a refresh token with all required scopes:

# Clone the repo and run the helper script
npx tsx scripts/get-refresh-token.ts

This will:

  • Start a local server to catch the OAuth callback
  • Open your browser to authorize the app
  • Automatically exchange the code for tokens
  • Output the refresh token to add to your .env file

Option B: Manual Setup

  1. In API Console, select your client → Generate CodeSelf Client
  2. Enter the required scopes (see below)
  3. Click Create and copy the authorization code
  4. Exchange it for a refresh token (within 2 minutes):
curl -X POST "https://accounts.zoho.com/oauth/v2/token" \
  -d "grant_type=authorization_code" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "code=YOUR_AUTH_CODE"
  1. Copy the refresh_token from the response (it doesn't expire unless revoked)

Required OAuth Scopes

For full V3 API functionality, request these scopes:

ZohoProjects.projects.ALL,ZohoProjects.tasks.ALL,ZohoProjects.tasklists.ALL,ZohoProjects.portals.READ,ZohoProjects.users.ALL,ZohoProjects.timesheets.ALL,ZohoProjects.bugs.ALL,ZohoProjects.milestones.ALL,ZohoProjects.events.ALL,ZohoProjects.forums.ALL,ZohoProjects.documents.ALL,ZohoProjects.tags.ALL,ZohoProjects.status.READ,ZohoProjects.search.READ

| Scope | Description | |-------|-------------| | ZohoProjects.projects.ALL | Create, read, update, delete projects | | ZohoProjects.tasks.ALL | Full task management | | ZohoProjects.tasklists.ALL | Create and manage task lists | | ZohoProjects.portals.READ | Read portal information | | ZohoProjects.users.ALL | User management | | ZohoProjects.timesheets.ALL | Time logging | | ZohoProjects.bugs.ALL | Issue/bug tracking | | ZohoProjects.milestones.ALL | Milestone/phase management | | ZohoProjects.events.ALL | Calendar events | | ZohoProjects.forums.ALL | Discussion forums | | ZohoProjects.documents.ALL | Document management | | ZohoProjects.tags.ALL | Tag management | | ZohoProjects.status.READ | Read status configurations | | ZohoProjects.search.READ | Global search |

Note: Missing scopes will result in INVALID_OAUTHSCOPE errors for specific operations

Configuration

const client = createZohoProjectsClient({
  // Required
  clientId: "your_client_id",
  clientSecret: "your_client_secret",
  refreshToken: "your_refresh_token",
  portalId: "your_portal_id",

  // Optional - defaults shown
  apiUrl: "https://projectsapi.zoho.com",      // US region
  accountsUrl: "https://accounts.zoho.com",    // US region
  timeout: 30000,                               // 30 seconds

  // Optional - for distributed rate limiting
  redis: {
    url: "redis://localhost:6379",
  },
});

Region URLs

| Region | API URL | Accounts URL | |--------|---------|--------------| | US | https://projectsapi.zoho.com | https://accounts.zoho.com | | EU | https://projectsapi.zoho.eu | https://accounts.zoho.eu | | IN | https://projectsapi.zoho.in | https://accounts.zoho.in | | AU | https://projectsapi.zoho.com.au | https://accounts.zoho.com.au |

Available APIs

The client provides access to 26 API namespaces:

Portal-Level APIs (no projectId required)

| Namespace | Description | |-----------|-------------| | projects | Project management | | users | Portal users | | tags | Tags/labels | | roles | User roles | | profiles | Permission profiles | | clients | Client companies | | contacts | Client contacts | | groups | Project groups | | leaves | Leave requests | | teams | Team management | | portals | Portal information | | modules | Portal modules | | dashboards | Dashboards & widgets | | reports | Report execution | | search | Global search | | trash | Deleted items |

Project-Scoped APIs (require projectId)

| Namespace | Description | |-----------|-------------| | tasks | Task management | | tasklists | Task list management | | phases | Project phases/milestones | | issues | Bug/issue tracking | | forums | Discussion forums | | events | Calendar events | | timelogs | Time tracking | | timers | Active timers | | attachments | File attachments | | documents | Project documents | | blueprints | Workflow blueprints | | customviews | Custom views |

Polymorphic APIs

| Namespace | Description | |-----------|-------------| | comments | Comments on tasks, issues, forums | | followers | Followers on tasks, issues, forums |

API Reference

Projects

// List with pagination (V3 API uses page/per_page)
const { data, pageInfo } = await client.projects.list({ page: 1, per_page: 100 });

// Get all projects (auto-pagination)
const projects = await client.projects.listAll();

// Iterate (memory-efficient)
for await (const project of client.projects.iterate()) {
  console.log(project.name);
}

// CRUD operations
const project = await client.projects.get("project_id");
const newProject = await client.projects.create({ name: "My Project" });
await client.projects.update("project_id", { name: "Updated Name" });
await client.projects.delete("project_id");

Tasks

// List tasks for a project
const { data } = await client.tasks.list("project_id");

// Get all tasks for a project
const tasks = await client.tasks.listAll("project_id");

// Get all tasks across all projects
const allTasks = await client.tasks.listAllAcrossProjects();

// CRUD operations
const task = await client.tasks.get("project_id", "task_id");
await client.tasks.create("project_id", {
  name: "New Task",
  tasklist: { id: "list_id" },  // V3 API uses nested object
});
await client.tasks.update("project_id", "task_id", { status: "completed" });
await client.tasks.delete("project_id", "task_id");

Time Logs

Time logs require specific parameters:

// List time logs for a project
const { data } = await client.timelogs.list("project_id", {
  users_list: "all",              // "all" or comma-separated user IDs
  view_type: "month",             // "day", "week", "month", or "custom_date"
  date: "01-15-2025",             // MM-DD-YYYY format
  bill_status: "All",             // "All", "Billable", or "Non Billable"
  component_type: "task",         // "task", "bug", or "general"
});

// Create time logs
await client.timelogs.createForTask("project_id", {
  task_id: "task_id",
  date: "01-15-2025",
  hours: "2",
  bill_status: "Billable",
});

await client.timelogs.createForBug("project_id", {
  bug_id: "bug_id",
  date: "01-15-2025",
  hours: "1",
  bill_status: "Non Billable",
});

await client.timelogs.createGeneral("project_id", {
  name: "Meeting",
  date: "01-15-2025",
  hours: "1",
  bill_status: "Non Billable",
});

Issues (Bugs)

const { data } = await client.issues.list("project_id");
const issue = await client.issues.get("project_id", "issue_id");
await client.issues.create("project_id", { title: "Bug report" });
await client.issues.update("project_id", "issue_id", { status: "fixed" });
await client.issues.delete("project_id", "issue_id");

Comments (Polymorphic)

// Comments on tasks
const taskComments = client.comments.forTask("project_id", "task_id");
const { data } = await taskComments.list();
await taskComments.create({ content: "Great work!" });

// Comments on issues
const issueComments = client.comments.forIssue("project_id", "issue_id");
await issueComments.list();

// Comments on forums
const forumComments = client.comments.forForum("project_id", "forum_id");
await forumComments.list();

Followers (Polymorphic)

// Followers on tasks
const taskFollowers = client.followers.forTask("project_id", "task_id");
const { data } = await taskFollowers.list();
await taskFollowers.add({ user_ids: ["user_1", "user_2"] });
await taskFollowers.remove("user_id");

// Followers on issues
const issueFollowers = client.followers.forIssue("project_id", "issue_id");
await issueFollowers.list();

Search

// Global search
const results = await client.search.query({ search_term: "keyword" });

// Search with filters
const taskResults = await client.search.query({
  search_term: "keyword",
  entity_type: "task",
});

// Convenience methods
await client.search.tasks("keyword");
await client.search.issues("keyword");
await client.search.projects("keyword");

// Search within a project
await client.search.inProject("project_id", { search_term: "keyword" });

Trash

// List deleted items
const { data } = await client.trash.list();
const projectTrash = await client.trash.list({ entity_type: "task" });

// Restore from trash
await client.trash.restore("task", "item_id");

// Permanently delete
await client.trash.permanentDelete("task", "item_id");

// Empty trash
await client.trash.empty();           // All items
await client.trash.empty("task");     // Only tasks

Timers

// Get active timer for current user
const timer = await client.timers.getActive("project_id");

// Start/stop timer on a task
await client.timers.startForTask("project_id", "task_id");
await client.timers.stop("project_id");

// Start/stop timer on a bug
await client.timers.startForBug("project_id", "bug_id");

Teams

const { data } = await client.teams.list();
const team = await client.teams.get("team_id");
await client.teams.create({ name: "Engineering" });
await client.teams.addMembers("team_id", { user_ids: ["user_1", "user_2"] });
await client.teams.removeMember("team_id", "user_id");

Dashboards & Widgets

// Dashboards
const { data } = await client.dashboards.list();
const dashboard = await client.dashboards.get("dashboard_id");

// Widgets within a dashboard
const widgets = client.dashboards.widgets("dashboard_id");
const { data: widgetList } = await widgets.list();
await widgets.create({ name: "Task Chart", type: "chart" });

Rate Limiting

The client automatically handles Zoho's rate limits:

  • 100 requests per 2 minutes
  • 30-minute lockout on 429 response

Uses Bottleneck with a safety margin (90 requests per 2 minutes).

For distributed deployments, configure Redis:

const client = createZohoProjectsClient({
  // ...
  redis: {
    url: process.env.REDIS_URL!,
  },
});

Error Handling

import {
  ZohoProjectsError,
  ZohoAuthenticationError,
  ZohoRateLimitError,
  ZohoNotFoundError,
  isRateLimitError,
} from "@panoptic-it-solutions/zoho-projects-client";

try {
  const project = await client.projects.get("invalid_id");
} catch (error) {
  if (error instanceof ZohoNotFoundError) {
    console.log("Project not found");
  } else if (isRateLimitError(error)) {
    console.log(`Rate limited, retry after ${error.lockoutDurationMs}ms`);
  } else if (error instanceof ZohoAuthenticationError) {
    console.log("Invalid credentials");
  }
}

Environment Variables

ZOHO_CLIENT_ID=your_client_id
ZOHO_CLIENT_SECRET=your_client_secret
ZOHO_REFRESH_TOKEN=your_refresh_token
ZOHO_PORTAL_ID=your_portal_id

# Optional - defaults to US region
ZOHO_API_URL=https://projectsapi.zoho.com
ZOHO_ACCOUNTS_URL=https://accounts.zoho.com

Changelog

1.1.0 (2026-01-13)

V3 API Response Format Fixes

  • Fixed count field type coercion: Changed all count fields (open, closed, *_count, page, per_page, total_count) to use z.coerce.number() to handle Zoho API returning counts as strings (e.g., "0" instead of 0)
  • Fixed V3 response parsing: V3 API returns data as direct arrays, not wrapped in objects like { projects: [...] }. Updated parsing logic throughout.

File Attachments

  • Added file attachment workflow documentation: Complete guide for attaching files to tasks using WorkDrive integration
  • Method 1 (Recommended): Direct upload via legacy /restapi/ endpoint with uploaddoc form field - single request handles both upload and association
  • Method 2 (Advanced): Manual WorkDrive upload with separate registration step for more control
  • Added OAuth scopes for attachments: ZohoPC.files.ALL, WorkDrive.team.ALL, WorkDrive.workspace.ALL, WorkDrive.files.ALL, WorkDrive.teamfolders.ALL

Helper Scripts

  • scripts/get-refresh-token.ts: OAuth helper that opens browser, catches callback, and outputs refresh token with all required scopes
  • scripts/delete-test-project.ts: Delete projects using V3 API (POST /projects/{id}/trash)
  • scripts/test-workdrive-upload.ts: Test WorkDrive upload and task attachment workflow

V3 API Discoveries

  • Project delete: V3 uses POST /api/v3/portal/{portalId}/projects/{projectId}/trash (not DELETE)
  • Attachments: V3 returns { attachment: [...] } (singular key), not { attachments: [...] }
  • Projects list: V3 returns direct array, not wrapped in { projects: [...] }

License

MIT