Install
openclaw skills install zoho-projectsZoho Projects API integration with managed OAuth. Manage projects, tasks, milestones, tasklists, and team collaboration. Use this skill when users want to manage project tasks, track time, organize milestones, or collaborate on projects. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway).
openclaw skills install zoho-projectsAccess the Zoho Projects API with managed OAuth authentication. Manage projects, tasks, milestones, tasklists, and team collaboration.
# List all portals
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://api.maton.ai/zoho-projects/restapi/portals/')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
https://api.maton.ai/zoho-projects/{native-api-path}
Maton proxies requests to projectsapi.zoho.com and automatically injects your OAuth token.
All requests require the Maton API key in the Authorization header:
Authorization: Bearer $MATON_API_KEY
Environment Variable: Set your API key as MATON_API_KEY:
export MATON_API_KEY="YOUR_API_KEY"
Manage your Zoho Projects OAuth connections at https://api.maton.ai.
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://api.maton.ai/connections?app=zoho-projects&status=ACTIVE')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
python <<'EOF'
import urllib.request, os, json
data = json.dumps({'app': 'zoho-projects'}).encode()
req = urllib.request.Request('https://api.maton.ai/connections', data=data, method='POST')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Content-Type', 'application/json')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://api.maton.ai/connections/{connection_id}')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Response:
{
"connection": {
"connection_id": "{connection_id}",
"status": "ACTIVE",
"creation_time": "2026-02-28T00:12:25.223434Z",
"last_updated_time": "2026-02-28T00:16:32.882675Z",
"url": "https://connect.maton.ai/?session_token=...",
"app": "zoho-projects",
"metadata": {},
"method": "OAUTH2"
}
}
Open the returned url in a browser to complete OAuth authorization.
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://api.maton.ai/connections/{connection_id}', method='DELETE')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
If you have multiple Zoho Projects connections, specify which one to use with the Maton-Connection header:
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://api.maton.ai/zoho-projects/restapi/portals/')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
req.add_header('Maton-Connection', '{connection_id}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
If you have multiple connections, always include this header to ensure requests go to the intended account.
GET /zoho-projects/restapi/portals/
Response:
{
"portals": [
{
"id": 916020774,
"id_string": "916020774",
"name": "nunchidotapp",
"plan": "Ultimate",
"role": "admin",
"project_count": {"active": 1}
}
]
}
GET /zoho-projects/restapi/portal/{portal_id}/
GET /zoho-projects/restapi/portal/{portal_id}/projects/
Query parameters: index, range, status, sort_column, sort_order
Response:
{
"projects": [
{
"id": 2644874000000089119,
"name": "My Project",
"status": "active",
"owner_name": "Byungkyu Park",
"task_count": {"open": 3, "closed": 0},
"project_percent": "0"
}
]
}
GET /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/
POST /zoho-projects/restapi/portal/{portal_id}/projects/
Content-Type: application/x-www-form-urlencoded
name=New+Project&owner={user_id}&description=Project+description
Required: name
Optional: owner, description, start_date, end_date, template_id, group_id
Response:
{
"projects": [
{
"id": 2644874000000096003,
"name": "New Project",
"status": "active"
}
]
}
POST /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/
Content-Type: application/x-www-form-urlencoded
name=Updated+Name&status=active
DELETE /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/
Response:
{"response": "Project Trashed successfully"}
GET /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasks/
Query parameters: index, range, owner, status, priority, tasklist_id, sort_column
Response:
{
"tasks": [
{
"id": 2644874000000089255,
"name": "Task 3",
"status": {"name": "Open", "type": "open"},
"priority": "None",
"completed": false,
"tasklist": {"name": "General", "id": "2644874000000089245"}
}
]
}
GET /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasks/{task_id}/
GET /zoho-projects/restapi/portal/{portal_id}/mytasks/
Query parameters: index, range, owner, status, priority, projects_ids
POST /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasks/
Content-Type: application/x-www-form-urlencoded
name=New+Task&tasklist_id={tasklist_id}&priority=High
Required: name
Optional: person_responsible, tasklist_id, start_date, end_date, priority, description
Response:
{
"tasks": [
{
"id": 2644874000000094001,
"key": "EZ1-T4",
"name": "New Task",
"status": {"name": "Open", "type": "open"}
}
]
}
POST /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasks/{task_id}/
Content-Type: application/x-www-form-urlencoded
name=Updated+Name&priority=High&percent_complete=50
DELETE /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasks/{task_id}/
Response:
{"response": "Task Trashed successfully"}
GET /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasks/{task_id}/comments/
POST /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasks/{task_id}/comments/
Content-Type: application/x-www-form-urlencoded
content=This+is+a+comment
Response:
{
"comments": [
{
"id": 2644874000000094015,
"content": "This is a comment",
"added_person": "Byungkyu Park"
}
]
}
POST /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasks/{task_id}/comments/{comment_id}/
Content-Type: application/x-www-form-urlencoded
content=Updated+comment
DELETE /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasks/{task_id}/comments/{comment_id}/
GET /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasklists/
Query parameters: index, range, flag, milestone_id, sort_column
Response:
{
"tasklists": [
{
"id": 2644874000000089245,
"name": "General",
"flag": "internal",
"is_default": true,
"task_count": {"open": 3}
}
]
}
POST /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasklists/
Content-Type: application/x-www-form-urlencoded
name=New+Tasklist&flag=internal
Required: name
Optional: milestone_id, flag
POST /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasklists/{tasklist_id}/
Content-Type: application/x-www-form-urlencoded
name=Updated+Name&milestone_id={milestone_id}
DELETE /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasklists/{tasklist_id}/
Response:
{"response": "Tasklist Trashed successfully"}
GET /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/milestones/
Query parameters: index, range, status, flag, sort_column
Note: Returns 204 No Content if no milestones exist.
POST /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/milestones/
Content-Type: application/x-www-form-urlencoded
name=Phase+1&start_date=03-01-2026&end_date=03-15-2026&owner={user_id}&flag=internal
Required: name, start_date, end_date, owner, flag
Response:
{
"milestones": [
{
"id": 2644874000000096133,
"name": "Phase 1",
"start_date": "03-01-2026",
"end_date": "03-15-2026",
"status": "notcompleted"
}
]
}
POST /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/milestones/{milestone_id}/
Content-Type: application/x-www-form-urlencoded
name=Updated+Phase&start_date=03-01-2026&end_date=03-20-2026&owner={user_id}&flag=internal
POST /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/milestones/{milestone_id}/status/
Content-Type: application/x-www-form-urlencoded
status=2
Status values: 1 = not completed, 2 = completed
DELETE /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/milestones/{milestone_id}/
Response:
{"response": "Milestone Trashed successfully"}
GET /zoho-projects/restapi/portal/{portal_id}/users/
Response:
{
"users": [
{
"id": "801698114",
"zpuid": "2644874000000085003",
"name": "Byungkyu Park",
"email": "byungkyu@example.com",
"role_name": "Administrator",
"active": true
}
]
}
GET /zoho-projects/restapi/portal/{portal_id}/projects/groups
Response:
{
"groups": [
{
"id": "2644874000000018001",
"name": "Ungrouped Projects",
"is_default": "true"
}
]
}
POST /zoho-projects/restapi/portal/{portal_id}/projectgroups
Content-Type: application/x-www-form-urlencoded
name=New+Group
Use index and range parameters for pagination:
GET /zoho-projects/restapi/portal/{portal_id}/projects/{project_id}/tasks/?index=1&range=50
index: Starting record number (1-based)range: Number of records to return// List tasks in a project
const response = await fetch(
'https://api.maton.ai/zoho-projects/restapi/portal/916020774/projects/2644874000000089119/tasks/',
{
headers: {
'Authorization': `Bearer ${process.env.MATON_API_KEY}`
}
}
);
const data = await response.json();
console.log(data.tasks);
import os
import requests
# Create a task
response = requests.post(
'https://api.maton.ai/zoho-projects/restapi/portal/916020774/projects/2644874000000089119/tasks/',
headers={
'Authorization': f'Bearer {os.environ["MATON_API_KEY"]}',
'Content-Type': 'application/x-www-form-urlencoded'
},
data={'name': 'New Task', 'priority': 'High'}
)
task = response.json()
print(task['tasks'][0]['id'])
application/x-www-form-urlencoded content type, not JSONGET /restapi/portals/MM-dd-yyyy (e.g., 03-01-2026)name, start_date, end_date, owner, flag| Status | Meaning |
|---|---|
| 204 | No content (empty list) |
| 400 | Missing/invalid input parameter |
| 401 | Invalid or missing Maton API key |
| 404 | Resource not found |
| 429 | Rate limited (100 requests per 2 minutes) |
| 4xx/5xx | Passthrough error from Zoho Projects API |
Common error codes:
6831: Input Parameter Missing6832: Input Parameter Does not Match the Pattern SpecifiedMATON_API_KEY environment variable is set:echo $MATON_API_KEY
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://api.maton.ai/connections')
req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}')
print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))
EOF
Ensure your URL path starts with zoho-projects. For example:
https://api.maton.ai/zoho-projects/restapi/portals/https://api.maton.ai/restapi/portals/