TypeScript client for Zoho Projects V3 API with OAuth 2.0 and rate limiting
npm install @panoptic-it-solutions/zoho-projects-clientTypeScript client for Zoho Projects V3 API with OAuth 2.0 and rate limiting.
``bash`
npm install @panoptic-it-solutions/zoho-projects-client
For projects using Claude Code or other AI coding assistants, run the init command to set up helpful slash commands and documentation:
`bash`
npx @panoptic-it-solutions/zoho-projects-client init
| 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 |
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
`bashInclude type definitions for enhanced AI visibility
npx @panoptic-it-solutions/zoho-projects-client init --with-types
The
--with-types option copies TypeScript definitions to .ai-types/ for AI assistants that benefit from having type information in the project.Quick Start
`typescript
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:
$3
1. Go to Zoho API Console (or
.eu, .in, .com.au for your region)
2. Click Add Client → Server-based Applications
3. Fill in the details and click Create
4. Copy the Client ID and Client Secret$3
#### Option A: Using the Helper Script (Recommended)
The easiest way to get a refresh token with all required scopes:
`bash
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 Code → Self 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):
`bash
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"
`5. Copy the
refresh_token from the response (it doesn't expire unless revoked)$3
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 operationsConfiguration
`typescript
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",
},
});
`$3
| 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:
$3
| 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 |$3
| 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 |$3
| Namespace | Description |
|-----------|-------------|
|
comments | Comments on tasks, issues, forums |
| followers | Followers on tasks, issues, forums |API Reference
$3
`typescript
// 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");
`$3
`typescript
// 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");
`$3
Time logs require specific parameters:
`typescript
// 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",
});
`$3
`typescript
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");
`$3
`typescript
// 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();
`$3
`typescript
// 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();
`$3
`typescript
// 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" });
`$3
`typescript
// 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
`$3
`typescript
// 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");
`$3
`typescript
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");
`$3
`typescript
// 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:
`typescript
const client = createZohoProjectsClient({
// ...
redis: {
url: process.env.REDIS_URL!,
},
});
`Error Handling
`typescript
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
`env
ZOHO_CLIENT_ID=your_client_id
ZOHO_CLIENT_SECRET=your_client_secret
ZOHO_REFRESH_TOKEN=your_refresh_token
ZOHO_PORTAL_ID=your_portal_idOptional - defaults to US region
ZOHO_API_URL=https://projectsapi.zoho.com
ZOHO_ACCOUNTS_URL=https://accounts.zoho.com
`Changelog
$3
#### 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: [...] }`MIT