# Clockify Project API Guide
## Overview
The Project API manages projects, tasks, team member assignments, and project-specific rates within Clockify workspaces.
## Authentication
- `X-Api-Key`: Personal API key
- `X-Addon-Token`: For addon authentication
## Base URL
```
https://api.clockify.me/api/v1
```
---
## Endpoints
### 1. Get All Projects
**Method:** `GET`
**Path:** `/v1/workspaces/{workspaceId}/projects`
**Purpose:** List all projects in a workspace with filtering
#### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| name | string | No | Filter by project name substring |
| clients | string | No | Filter by client IDs (repeatable) |
| is-template | boolean | No | Filter template/non-template projects |
| archived | boolean | No | Filter archived/active projects |
| billable | boolean | No | Filter billable/non-billable projects |
| contains-client | boolean | No | Filter projects with/without client |
| client-status | string | No | Filter by client status (ACTIVE, ARCHIVED) |
| users | string | No | Filter by user IDs with access (repeatable) |
| user-groups | string | No | Filter by user group IDs (repeatable) |
| contains-user | boolean | No | Filter projects with/without users |
| sort-column | string | No | Sort by: NAME, CLIENT_NAME |
| sort-order | string | No | ASCENDING or DESCENDING |
| page | integer | No | Page number (default: 1) |
| page-size | integer | No | Page size (default: 50) |
| hydrated | boolean | No | Include full details |
| favorite | boolean | No | Filter favorite projects for user |
| include-memberships | string | No | Include membership info: ALL, NONE |
#### Request Example
```http
GET /v1/workspaces/64a687e29ae1f428e7ebe303/projects?archived=false&billable=true&page-size=100
X-Api-Key: YOUR_API_KEY
```
#### Response (200 OK)
```json
[
{
"archived": false,
"billable": true,
"clientId": "64c777ddd3fcab07cfbb210c",
"clientName": "Acme Corp",
"color": "#0B83D9",
"costRate": {
"amount": 10000,
"currency": "USD"
},
"duration": "PT100H30M",
"estimate": {
"estimate": "PT200H",
"type": "AUTO"
},
"hourlyRate": {
"amount": 15000,
"currency": "USD"
},
"id": "5b641568b07987035750505e",
"memberships": [
{
"costRate": {
"amount": 10000,
"currency": "USD"
},
"hourlyRate": {
"amount": 15000,
"currency": "USD"
},
"membershipStatus": "ACTIVE",
"membershipType": "PROJECT",
"targetId": "5b641568b07987035750505e",
"userId": "5a0ab5acb07987125438b60f"
}
],
"name": "Website Redesign",
"note": "Q1 2024 project",
"public": true,
"template": false,
"timeEstimate": {
"active": true,
"estimate": "PT200H",
"includeNonBillable": false,
"resetOption": "MONTHLY",
"type": "AUTO"
},
"workspaceId": "64a687e29ae1f428e7ebe303"
}
]
```
---
### 2. Add Project
**Method:** `POST`
**Path:** `/v1/workspaces/{workspaceId}/projects`
**Purpose:** Create a new project
#### Request Body
```json
{
"name": "Mobile App Development",
"clientId": "64c777ddd3fcab07cfbb210c",
"isPublic": true,
"estimate": {
"estimate": "PT300H",
"type": "AUTO"
},
"color": "#FF5722",
"note": "iOS and Android app",
"billable": true,
"public": true,
"budgetEstimate": {
"active": true,
"estimate": 50000,
"resetOption": "MONTHLY",
"type": "MANUAL"
},
"customFields": [
{
"customFieldId": "5e4117fe8c625f38930d57b7",
"value": "PRJ-001"
}
]
}
```
#### Field Definitions
- **name**: Required, 1-250 characters
- **clientId**: Optional client association
- **isPublic**: Required, determines visibility
- **estimate**: Optional time estimate
- **color**: Optional hex color code
- **note**: Optional project description
- **billable**: Optional, defaults to workspace setting
- **budgetEstimate**: Optional budget configuration
- **customFields**: Optional custom field values
#### Response (201 Created)
Returns created project object (same structure as GET response)
---
### 3. Get Project by ID
**Method:** `GET`
**Path:** `/v1/workspaces/{workspaceId}/projects/{projectId}`
**Purpose:** Retrieve details for a specific project
#### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| hydrated | boolean | No | Include full membership details |
#### Response (200 OK)
Returns single project object
---
### 4. Update Project
**Method:** `PUT`
**Path:** `/v1/workspaces/{workspaceId}/projects/{projectId}`
**Purpose:** Update project details
#### Request Body
```json
{
"name": "Mobile App Development - Updated",
"clientId": "64c777ddd3fcab07cfbb210c",
"isPublic": true,
"billable": true,
"color": "#FF5722",
"note": "Updated project scope",
"archived": false,
"customFields": [
{
"customFieldId": "5e4117fe8c625f38930d57b7",
"value": "PRJ-001-UPDATED"
}
]
}
```
#### Response (200 OK)
Returns updated project object
---
### 5. Delete Project
**Method:** `DELETE`
**Path:** `/v1/workspaces/{workspaceId}/projects/{projectId}`
**Purpose:** Delete a project from workspace
#### Response
204 No Content (success)
#### Constraints
- Deletes all associated tasks
- Deletes all time entries on the project
- Cannot be undone
- ⚠️ Use with extreme caution
---
### 6. Update Project Estimate
**Method:** `PATCH`
**Path:** `/v1/workspaces/{workspaceId}/projects/{projectId}/estimate`
**Purpose:** Update project time or budget estimate
#### Request Body
```json
{
"estimate": {
"estimate": "PT250H",
"type": "AUTO"
},
"budgetEstimate": {
"active": true,
"estimate": 75000,
"includeExpenses": true,
"resetOption": "MONTHLY",
"type": "MANUAL"
},
"timeEstimate": {
"active": true,
"estimate": "PT250H",
"includeNonBillable": false,
"resetOption": "MONTHLY",
"type": "AUTO"
}
}
```
#### Estimate Types
- **AUTO**: Automatically calculated from tasks
- **MANUAL**: Manually set estimate
#### Reset Options
- **MONTHLY**: Reset estimate monthly
- **BIWEEKLY**: Reset every two weeks
- **WEEKLY**: Reset weekly
- **NEVER**: Never reset
#### Response (200 OK)
Returns updated project object
---
### 7. Update Project Memberships
**Method:** `PATCH`
**Path:** `/v1/workspaces/{workspaceId}/projects/{projectId}/memberships`
**Purpose:** Update user memberships and their project-specific rates
#### Request Body
```json
{
"memberships": [
{
"userId": "5a0ab5acb07987125438b60f",
"hourlyRate": {
"amount": 15000,
"currency": "USD"
},
"costRate": {
"amount": 10000,
"currency": "USD"
},
"membershipStatus": "ACTIVE",
"membershipType": "PROJECT"
}
]
}
```
#### Response (200 OK)
Returns updated project object with memberships
---
### 8. Assign/Remove Users
**Method:** `POST`
**Path:** `/v1/workspaces/{workspaceId}/projects/{projectId}/users`
**Purpose:** Batch assign or remove users from project
#### Request Body
```json
{
"memberships": [
{
"userId": "5a0ab5acb07987125438b60f",
"membershipStatus": "ACTIVE",
"membershipType": "PROJECT",
"hourlyRate": {
"amount": 15000,
"currency": "USD"
}
}
]
}
```
#### Response (200 OK)
Returns updated project object
#### Use Cases
- Add multiple team members at once
- Set project-specific rates during assignment
- Update membership statuses
---
### 9. Update Project User Cost Rate
**Method:** `PUT`
**Path:** `/v1/workspaces/{workspaceId}/projects/{projectId}/users/{userId}/cost-rate`
**Purpose:** Set project-specific cost rate for a user
#### Request Body
```json
{
"amount": 12000
}
```
#### Response (200 OK)
Returns updated project object
#### Rate Hierarchy
1. Task-specific rate
2. **Project-specific user rate** (this endpoint)
3. User workspace rate
4. Workspace default rate
---
### 10. Update Project User Billable Rate
**Method:** `PUT`
**Path:** `/v1/workspaces/{workspaceId}/projects/{projectId}/users/{userId}/hourly-rate`
**Purpose:** Set project-specific billable rate for a user
#### Request Body
```json
{
"amount": 18000
}
```
#### Response (200 OK)
Returns updated project object
---
### 11. Update Project Template
**Method:** `PATCH`
**Path:** `/v1/workspaces/{workspaceId}/projects/{projectId}/template`
**Purpose:** Convert project to/from template
#### Request Body
```json
{
"isTemplate": true
}
```
#### Response (200 OK)
Returns updated project object
#### Template Features
- Templates can be used to create new projects
- Template projects don't appear in regular project lists
- Useful for standardizing project structure
---
## Data Models
### Project Object
```typescript
{
archived: boolean;
billable: boolean;
budgetEstimate?: BudgetEstimate;
clientId?: string;
clientName?: string;
color: string; // Hex color code
costRate?: Rate;
duration?: string; // ISO-8601 duration
estimate?: {
estimate: string; // ISO-8601 duration
type: "AUTO" | "MANUAL";
};
hourlyRate?: Rate;
id: string;
memberships?: Membership[];
name: string;
note?: string;
public: boolean;
template: boolean;
timeEstimate?: TimeEstimate;
workspaceId: string;
}
```
### TimeEstimate Object
```typescript
{
active: boolean;
estimate: string; // ISO-8601 duration
includeNonBillable: boolean;
resetOption: "MONTHLY" | "BIWEEKLY" | "WEEKLY" | "NEVER";
type: "AUTO" | "MANUAL";
}
```
### BudgetEstimate Object
```typescript
{
active: boolean;
estimate: number; // Amount in cents
includeExpenses: boolean;
resetOption: "MONTHLY" | "BIWEEKLY" | "WEEKLY" | "NEVER";
type: "AUTO" | "MANUAL";
}
```
### Membership Object
```typescript
{
costRate?: Rate;
hourlyRate?: Rate;
membershipStatus: "ACTIVE" | "PENDING" | "DECLINED" | "INACTIVE";
membershipType: "PROJECT" | "WORKSPACE" | "USERGROUP";
targetId: string;
userId: string;
}
```
---
## Implementation Examples
### Python: Create Project with Team
```python
import requests
API_KEY = "your_api_key_here"
BASE_URL = "https://api.clockify.me/api/v1"
headers = {
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
}
def create_project(workspace_id, name, client_id=None, is_public=True, billable=True):
url = f"{BASE_URL}/workspaces/{workspace_id}/projects"
payload = {
"name": name,
"isPublic": is_public,
"billable": billable,
"color": "#0B83D9"
}
if client_id:
payload["clientId"] = client_id
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 201:
return response.json()
else:
raise Exception(f"Failed to create project: {response.text}")
def add_team_to_project(workspace_id, project_id, user_ids, hourly_rate_cents=None):
url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/users"
memberships = []
for user_id in user_ids:
membership = {
"userId": user_id,
"membershipStatus": "ACTIVE",
"membershipType": "PROJECT"
}
if hourly_rate_cents:
membership["hourlyRate"] = {
"amount": hourly_rate_cents,
"currency": "USD"
}
memberships.append(membership)
payload = {"memberships": memberships}
response = requests.post(url, headers=headers, json=payload)
return response.json()
# Create project and add team
project = create_project(
"workspace_id",
"Q1 Marketing Campaign",
client_id="client_id",
billable=True
)
# Add team members with rates
team_user_ids = ["user1", "user2", "user3"]
add_team_to_project(
"workspace_id",
project['id'],
team_user_ids,
hourly_rate_cents=15000 # $150/hour
)
```
### Python: Get Projects with Filtering
```python
def get_active_billable_projects(workspace_id, client_id=None):
"""Get all active billable projects"""
url = f"{BASE_URL}/workspaces/{workspace_id}/projects"
params = {
"archived": "false",
"billable": "true",
"page-size": 1000,
"hydrated": "true"
}
if client_id:
params["clients"] = client_id
response = requests.get(url, headers=headers, params=params)
return response.json()
def get_user_projects(workspace_id, user_id):
"""Get all projects a user has access to"""
url = f"{BASE_URL}/workspaces/{workspace_id}/projects"
params = {
"users": user_id,
"archived": "false",
"page-size": 1000
}
response = requests.get(url, headers=headers, params=params)
return response.json()
# Get all billable projects
projects = get_active_billable_projects("workspace_id")
print(f"Found {len(projects)} billable projects")
# Get projects for specific user
user_projects = get_user_projects("workspace_id", "user_id")
```
### Python: Update Project Estimates
```python
def update_time_estimate(workspace_id, project_id, hours, estimate_type="AUTO", reset_option="MONTHLY"):
"""Update project time estimate"""
url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/estimate"
payload = {
"timeEstimate": {
"active": True,
"estimate": f"PT{hours}H",
"includeNonBillable": False,
"resetOption": reset_option,
"type": estimate_type
}
}
response = requests.patch(url, headers=headers, json=payload)
return response.json()
def update_budget_estimate(workspace_id, project_id, amount_cents, include_expenses=True, reset_option="MONTHLY"):
"""Update project budget estimate"""
url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/estimate"
payload = {
"budgetEstimate": {
"active": True,
"estimate": amount_cents,
"includeExpenses": include_expenses,
"resetOption": reset_option,
"type": "MANUAL"
}
}
response = requests.patch(url, headers=headers, json=payload)
return response.json()
# Set 200 hour time estimate
update_time_estimate("workspace_id", "project_id", 200)
# Set $50,000 budget
update_budget_estimate("workspace_id", "project_id", 5000000) # $50,000 in cents
```
### Python: Manage Project Team and Rates
```python
def set_project_user_rate(workspace_id, project_id, user_id, hourly_rate_cents, cost_rate_cents=None):
"""Set project-specific rates for a user"""
# Update hourly rate
hourly_url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/users/{user_id}/hourly-rate"
hourly_payload = {"amount": hourly_rate_cents}
requests.put(hourly_url, headers=headers, json=hourly_payload)
# Update cost rate if provided
if cost_rate_cents:
cost_url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/users/{user_id}/cost-rate"
cost_payload = {"amount": cost_rate_cents}
requests.put(cost_url, headers=headers, json=cost_payload)
def bulk_update_team_rates(workspace_id, project_id, user_rate_map):
"""Update rates for multiple users"""
url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/memberships"
memberships = []
for user_id, rates in user_rate_map.items():
membership = {
"userId": user_id,
"membershipStatus": "ACTIVE",
"membershipType": "PROJECT"
}
if "hourly_rate" in rates:
membership["hourlyRate"] = {
"amount": rates["hourly_rate"],
"currency": "USD"
}
if "cost_rate" in rates:
membership["costRate"] = {
"amount": rates["cost_rate"],
"currency": "USD"
}
memberships.append(membership)
payload = {"memberships": memberships}
response = requests.patch(url, headers=headers, json=payload)
return response.json()
# Set different rates for team members
rate_map = {
"user1": {"hourly_rate": 20000, "cost_rate": 10000}, # $200/hr billable, $100/hr cost
"user2": {"hourly_rate": 15000, "cost_rate": 8000}, # $150/hr billable, $80/hr cost
"user3": {"hourly_rate": 12000, "cost_rate": 6000} # $120/hr billable, $60/hr cost
}
bulk_update_team_rates("workspace_id", "project_id", rate_map)
```
### Python: Project Templates
```python
def create_project_from_template(workspace_id, template_name, new_project_name, client_id=None):
"""Create a new project based on a template"""
# Find template project
url = f"{BASE_URL}/workspaces/{workspace_id}/projects"
params = {"is-template": "true", "name": template_name}
response = requests.get(url, headers=headers, params=params)
templates = response.json()
if not templates:
raise Exception(f"Template '{template_name}' not found")
template = templates[0]
# Create new project with template settings
create_url = f"{BASE_URL}/workspaces/{workspace_id}/projects"
payload = {
"name": new_project_name,
"isPublic": template['public'],
"billable": template['billable'],
"color": template['color']
}
if client_id:
payload["clientId"] = client_id
if template.get('estimate'):
payload["estimate"] = template['estimate']
response = requests.post(create_url, headers=headers, json=payload)
new_project = response.json()
# Copy team members from template
if template.get('memberships'):
add_team_to_project(
workspace_id,
new_project['id'],
[m['userId'] for m in template['memberships']]
)
return new_project
# Create project from template
new_project = create_project_from_template(
"workspace_id",
"Standard Web Project Template",
"Client XYZ Website",
client_id="client_id"
)
```
### Python: Project Progress Tracking
```python
def get_project_progress(workspace_id, project_id):
"""Calculate project progress vs estimate"""
# Get project details
project_url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}"
project = requests.get(project_url, headers=headers).json()
# Parse duration and estimate
def parse_duration(duration_str):
"""Parse PT1H30M format to minutes"""
if not duration_str:
return 0
hours = 0
minutes = 0
if 'H' in duration_str:
hours = int(duration_str.split('H')[0].replace('PT', ''))
if 'M' in duration_str:
if 'H' in duration_str:
minutes = int(duration_str.split('H')[1].split('M')[0])
else:
minutes = int(duration_str.replace('PT', '').replace('M', ''))
return hours * 60 + minutes
actual_minutes = parse_duration(project.get('duration', 'PT0M'))
if project.get('timeEstimate') and project['timeEstimate'].get('estimate'):
estimate_minutes = parse_duration(project['timeEstimate']['estimate'])
progress_pct = (actual_minutes / estimate_minutes * 100) if estimate_minutes > 0 else 0
remaining_minutes = estimate_minutes - actual_minutes
else:
estimate_minutes = 0
progress_pct = 0
remaining_minutes = 0
return {
"project_name": project['name'],
"actual_hours": actual_minutes / 60,
"estimated_hours": estimate_minutes / 60,
"progress_percent": progress_pct,
"remaining_hours": remaining_minutes / 60,
"over_budget": actual_minutes > estimate_minutes
}
# Check project progress
progress = get_project_progress("workspace_id", "project_id")
print(f"Project: {progress['project_name']}")
print(f"Progress: {progress['progress_percent']:.1f}%")
print(f"Actual: {progress['actual_hours']:.1f} hours")
print(f"Estimated: {progress['estimated_hours']:.1f} hours")
print(f"Remaining: {progress['remaining_hours']:.1f} hours")
```
---
## Best Practices
1. **Public vs Private**: Use `isPublic=false` for confidential projects
2. **Client Association**: Link projects to clients for better organization
3. **Color Coding**: Use consistent color schemes for project types
4. **Estimates**: Set realistic time and budget estimates
5. **Rate Hierarchy**: Use project-specific rates strategically
6. **Templates**: Create templates for common project types
7. **Memberships**: Keep team assignments up to date
8. **Archiving**: Archive completed projects instead of deleting
---
## Common Use Cases
### Monthly Project Dashboard
```python
def monthly_project_dashboard(workspace_id, year, month):
"""Generate dashboard for all projects in a month"""
projects = get_active_billable_projects(workspace_id)
dashboard = []
for project in projects:
progress = get_project_progress(workspace_id, project['id'])
dashboard.append({
"name": project['name'],
"client": project.get('clientName', 'No Client'),
"billable": project['billable'],
"progress": progress['progress_percent'],
"over_budget": progress['over_budget'],
"team_size": len(project.get('memberships', []))
})
return dashboard
```
---
## Rate Limiting
- Addon token: 50 requests/second per workspace
---
## Notes
- Deleting a project deletes all time entries and tasks
- Project colors use hex format (#RRGGBB)
- Estimates reset based on configured schedule
- Templates don't appear in regular project listings
- Budget estimates include/exclude expenses based on settings
- Membership updates replace entire membership list