# Clockify API Quick Reference
## Common Operations Cheat Sheet
### Authentication Setup
```python
import requests
API_KEY = "your_api_key_here"
BASE_URL = "https://api.clockify.me/api/v1"
REPORTS_URL = "https://reports.api.clockify.me/v1"
headers = {
"X-Api-Key": API_KEY,
"Content-Type": "application/json"
}
```
---
## User Operations
### Get Current User
```python
GET /v1/user
response = requests.get(f"{BASE_URL}/user", headers=headers)
user = response.json()
# Returns: {id, name, email, defaultWorkspace, ...}
```
### List Workspace Users
```python
GET /v1/workspaces/{workspaceId}/users?status=ACTIVE
url = f"{BASE_URL}/workspaces/{workspace_id}/users"
params = {"status": "ACTIVE", "page-size": 100}
response = requests.get(url, headers=headers, params=params)
users = response.json()
```
### Update User Custom Field
```python
PUT /v1/workspaces/{workspaceId}/users/{userId}/custom-field/{customFieldId}/value
url = f"{BASE_URL}/workspaces/{workspace_id}/users/{user_id}/custom-field/{field_id}/value"
payload = {"value": "new_value"}
response = requests.put(url, headers=headers, json=payload)
```
---
## Workspace Operations
### List All Workspaces
```python
GET /v1/workspaces
response = requests.get(f"{BASE_URL}/workspaces", headers=headers)
workspaces = response.json()
```
### Create Workspace
```python
POST /v1/workspaces
payload = {
"name": "My Workspace",
"organizationId": "org_id"
}
response = requests.post(f"{BASE_URL}/workspaces", headers=headers, json=payload)
workspace = response.json()
```
### Add User to Workspace
```python
POST /v1/workspaces/{workspaceId}/users?send-email=true
url = f"{BASE_URL}/workspaces/{workspace_id}/users"
payload = {"email": "user@example.com"}
params = {"send-email": "true"}
response = requests.post(url, headers=headers, json=payload, params=params)
```
### Update Workspace Rate
```python
PUT /v1/workspaces/{workspaceId}/hourly-rate
url = f"{BASE_URL}/workspaces/{workspace_id}/hourly-rate"
payload = {
"amount": 15000, # $150.00
"currency": "USD",
"since": "2024-01-01T00:00:00Z"
}
response = requests.put(url, headers=headers, json=payload)
```
---
## Project Operations
### List Projects
```python
GET /v1/workspaces/{workspaceId}/projects?archived=false&billable=true
url = f"{BASE_URL}/workspaces/{workspace_id}/projects"
params = {
"archived": "false",
"billable": "true",
"page-size": 100
}
response = requests.get(url, headers=headers, params=params)
projects = response.json()
```
### Create Project
```python
POST /v1/workspaces/{workspaceId}/projects
url = f"{BASE_URL}/workspaces/{workspace_id}/projects"
payload = {
"name": "Website Redesign",
"clientId": "client_id", # Optional
"isPublic": True,
"billable": True,
"color": "#0B83D9"
}
response = requests.post(url, headers=headers, json=payload)
project = response.json()
```
### Add Team to Project
```python
POST /v1/workspaces/{workspaceId}/projects/{projectId}/users
url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/users"
payload = {
"memberships": [
{
"userId": "user1_id",
"membershipStatus": "ACTIVE",
"membershipType": "PROJECT",
"hourlyRate": {
"amount": 15000,
"currency": "USD"
}
}
]
}
response = requests.post(url, headers=headers, json=payload)
```
### Update Project Estimate
```python
PATCH /v1/workspaces/{workspaceId}/projects/{projectId}/estimate
url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/estimate"
payload = {
"timeEstimate": {
"active": True,
"estimate": "PT200H", # 200 hours
"type": "AUTO",
"resetOption": "MONTHLY"
}
}
response = requests.patch(url, headers=headers, json=payload)
```
---
## Time Entry Operations
### Create Time Entry
```python
POST /v1/workspaces/{workspaceId}/time-entries
from datetime import datetime
url = f"{BASE_URL}/workspaces/{workspace_id}/time-entries"
payload = {
"start": "2024-01-29T09:00:00Z",
"end": "2024-01-29T17:00:00Z",
"billable": True,
"description": "Development work",
"projectId": "project_id",
"taskId": "task_id", # Optional
"tagIds": ["tag1_id", "tag2_id"] # Optional
}
response = requests.post(url, headers=headers, json=payload)
entry = response.json()
```
### Start Timer
```python
POST /v1/workspaces/{workspaceId}/time-entries
# Same endpoint, but omit "end" field
payload = {
"start": datetime.utcnow().isoformat() + "Z",
"description": "Working on feature",
"projectId": "project_id"
}
response = requests.post(url, headers=headers, json=payload)
```
### Stop Timer
```python
PATCH /v1/workspaces/{workspaceId}/user/{userId}/time-entries
url = f"{BASE_URL}/workspaces/{workspace_id}/user/{user_id}/time-entries"
payload = {
"end": datetime.utcnow().isoformat() + "Z"
}
response = requests.patch(url, headers=headers, json=payload)
```
### Get Time Entries
```python
GET /v1/workspaces/{workspaceId}/user/{userId}/time-entries
url = f"{BASE_URL}/workspaces/{workspace_id}/user/{user_id}/time-entries"
params = {
"start": "2024-01-01T00:00:00Z",
"end": "2024-01-31T23:59:59Z",
"page-size": 1000,
"hydrated": "true"
}
response = requests.get(url, headers=headers, params=params)
entries = response.json()
```
### Update Time Entry
```python
PUT /v1/workspaces/{workspaceId}/time-entries/{timeEntryId}
url = f"{BASE_URL}/workspaces/{workspace_id}/time-entries/{entry_id}"
payload = {
"start": "2024-01-29T09:00:00Z",
"end": "2024-01-29T18:00:00Z",
"description": "Updated description",
"projectId": "new_project_id"
}
response = requests.put(url, headers=headers, json=payload)
```
### Delete Time Entry
```python
DELETE /v1/workspaces/{workspaceId}/time-entries/{timeEntryId}
url = f"{BASE_URL}/workspaces/{workspace_id}/time-entries/{entry_id}"
response = requests.delete(url, headers=headers)
# Returns 204 No Content
```
### Bulk Edit Time Entries
```python
PUT /v1/workspaces/{workspaceId}/time-entries/bulk-edit
url = f"{BASE_URL}/workspaces/{workspace_id}/time-entries/bulk-edit"
payload = {
"timeEntryIds": ["entry1", "entry2", "entry3"],
"operations": [
{"op": "CHANGE-PROJECT", "value": "new_project_id"},
{"op": "CHANGE-BILLABLE-STATUS", "value": True},
{"op": "ADD-TAGS", "value": ["tag1", "tag2"]}
]
}
response = requests.put(url, headers=headers, json=payload)
```
---
## Report Operations
### Detailed Report
```python
POST /workspaces/{workspaceId}/reports/detailed
url = f"{REPORTS_URL}/workspaces/{workspace_id}/reports/detailed"
payload = {
"dateRangeStart": "2024-01-01T00:00:00Z",
"dateRangeEnd": "2024-01-31T23:59:59Z",
"detailedFilter": {
"page": 1,
"pageSize": 1000,
"sortColumn": "DATE",
"sortOrder": "DESCENDING",
"options": {
"totals": "CALCULATE",
"includeTimeEntries": True
}
},
"amountShown": "EARNED",
"exportType": "JSON"
}
response = requests.post(url, headers=headers, json=payload)
report = response.json()
```
### Summary Report by Project
```python
POST /workspaces/{workspaceId}/reports/summary
url = f"{REPORTS_URL}/workspaces/{workspace_id}/reports/summary"
payload = {
"dateRangeStart": "2024-01-01T00:00:00Z",
"dateRangeEnd": "2024-01-31T23:59:59Z",
"summaryFilter": {
"groups": ["PROJECT", "USER"],
"sortColumn": "DURATION",
"sortOrder": "DESCENDING"
},
"amountShown": "EARNED",
"exportType": "JSON"
}
response = requests.post(url, headers=headers, json=payload)
summary = response.json()
```
### Weekly Report
```python
POST /workspaces/{workspaceId}/reports/weekly
url = f"{REPORTS_URL}/workspaces/{workspace_id}/reports/weekly"
payload = {
"dateRangeStart": "2024-01-01T00:00:00Z",
"dateRangeEnd": "2024-01-31T23:59:59Z",
"weeklyFilter": {
"sortColumn": "GROUP",
"sortOrder": "ASCENDING",
"groupByEntities": ["USER", "PROJECT"]
},
"amountShown": "EARNED",
"exportType": "JSON"
}
response = requests.post(url, headers=headers, json=payload)
weekly = response.json()
```
---
## Webhook Operations
### Create Webhook
```python
POST /v1/workspaces/{workspaceId}/webhooks
url = f"{BASE_URL}/workspaces/{workspace_id}/webhooks"
payload = {
"name": "Time Entry Monitor",
"url": "https://myapp.com/webhooks/clockify",
"webhookEvent": "NEW_TIME_ENTRY",
"triggerSourceType": "PROJECT_ID",
"triggerSource": ["project1_id", "project2_id"]
}
response = requests.post(url, headers=headers, json=payload)
webhook = response.json()
# IMPORTANT: Save webhook['authToken'] - only returned once!
```
### List Webhooks
```python
GET /v1/workspaces/{workspaceId}/webhooks
url = f"{BASE_URL}/workspaces/{workspace_id}/webhooks"
response = requests.get(url, headers=headers)
webhooks = response.json()
```
### Delete Webhook
```python
DELETE /v1/workspaces/{workspaceId}/webhooks/{webhookId}
url = f"{BASE_URL}/workspaces/{workspace_id}/webhooks/{webhook_id}"
response = requests.delete(url, headers=headers)
```
### Get Webhook Logs
```python
POST /v1/workspaces/{workspaceId}/webhooks/{webhookId}/logs
url = f"{BASE_URL}/workspaces/{workspace_id}/webhooks/{webhook_id}/logs"
payload = {
"from": "2024-01-01T00:00:00Z",
"to": "2024-01-31T23:59:59Z",
"status": "ALL",
"sortByNewest": True
}
params = {"page": 0, "size": 100}
response = requests.post(url, headers=headers, json=payload, params=params)
logs = response.json()
```
---
## Client Operations
### List Clients
```python
GET /v1/workspaces/{workspaceId}/clients
url = f"{BASE_URL}/workspaces/{workspace_id}/clients"
params = {"archived": "false", "page-size": 100}
response = requests.get(url, headers=headers, params=params)
clients = response.json()
```
### Create Client
```python
POST /v1/workspaces/{workspaceId}/clients
url = f"{BASE_URL}/workspaces/{workspace_id}/clients"
payload = {
"name": "Acme Corporation",
"email": "contact@acme.com", # Optional
"address": "123 Main St" # Optional
}
response = requests.post(url, headers=headers, json=payload)
client = response.json()
```
---
## Task Operations
### List Tasks
```python
GET /v1/workspaces/{workspaceId}/projects/{projectId}/tasks
url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/tasks"
params = {"is-active": "true", "page-size": 100}
response = requests.get(url, headers=headers, params=params)
tasks = response.json()
```
### Create Task
```python
POST /v1/workspaces/{workspaceId}/projects/{projectId}/tasks
url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/tasks"
payload = {
"name": "Design mockups",
"estimate": "PT8H", # Optional
"status": "ACTIVE"
}
response = requests.post(url, headers=headers, json=payload)
task = response.json()
```
---
## Tag Operations
### List Tags
```python
GET /v1/workspaces/{workspaceId}/tags
url = f"{BASE_URL}/workspaces/{workspace_id}/tags"
params = {"archived": "false", "page-size": 100}
response = requests.get(url, headers=headers, params=params)
tags = response.json()
```
### Create Tag
```python
POST /v1/workspaces/{workspaceId}/tags
url = f"{BASE_URL}/workspaces/{workspace_id}/tags"
payload = {"name": "Sprint 1"}
response = requests.post(url, headers=headers, json=payload)
tag = response.json()
```
---
## Helper Functions
### Parse Duration
```python
def parse_duration_to_hours(duration_str):
"""Parse PT1H30M format to decimal hours"""
if not duration_str or duration_str == "PT0S":
return 0.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 + (minutes / 60)
# Usage
duration = "PT2H30M"
hours = parse_duration_to_hours(duration) # Returns 2.5
```
### Format Duration
```python
def hours_to_duration(hours):
"""Convert decimal hours to PT format"""
h = int(hours)
m = int((hours - h) * 60)
if m > 0:
return f"PT{h}H{m}M"
else:
return f"PT{h}H"
# Usage
duration = hours_to_duration(2.5) # Returns "PT2H30M"
```
### Format Amount
```python
def cents_to_dollars(cents):
"""Convert cents to dollar string"""
return f"${cents / 100:.2f}"
def dollars_to_cents(dollars):
"""Convert dollars to cents"""
return int(dollars * 100)
# Usage
amount_str = cents_to_dollars(15000) # Returns "$150.00"
amount_cents = dollars_to_cents(150.00) # Returns 15000
```
### Date Range Helpers
```python
from datetime import datetime, timedelta
def get_month_range(year, month):
"""Get start and end of month"""
start = datetime(year, month, 1)
if month == 12:
end = datetime(year + 1, 1, 1) - timedelta(seconds=1)
else:
end = datetime(year, month + 1, 1) - timedelta(seconds=1)
return start, end
def get_week_range(date):
"""Get start (Monday) and end (Sunday) of week"""
start = date - timedelta(days=date.weekday())
end = start + timedelta(days=6, hours=23, minutes=59, seconds=59)
return start, end
# Usage
start, end = get_month_range(2024, 1)
```
---
## Complete Examples
### Daily Timer Workflow
```python
def daily_timer_workflow(workspace_id, user_id, project_id):
"""Complete daily timer workflow"""
# 1. Stop any running timer
stop_url = f"{BASE_URL}/workspaces/{workspace_id}/user/{user_id}/time-entries"
stop_payload = {"end": datetime.utcnow().isoformat() + "Z"}
requests.patch(stop_url, headers=headers, json=stop_payload)
# 2. Get today's entries
today = datetime.utcnow().date()
entries_url = f"{BASE_URL}/workspaces/{workspace_id}/user/{user_id}/time-entries"
entries_params = {
"start": f"{today.isoformat()}T00:00:00Z",
"end": f"{today.isoformat()}T23:59:59Z"
}
entries_response = requests.get(entries_url, headers=headers, params=entries_params)
entries = entries_response.json()
# 3. Calculate today's total
total_seconds = sum(
parse_duration_to_hours(e['timeInterval']['duration']) * 3600
for e in entries
)
total_hours = total_seconds / 3600
# 4. Start new timer
start_url = f"{BASE_URL}/workspaces/{workspace_id}/time-entries"
start_payload = {
"start": datetime.utcnow().isoformat() + "Z",
"description": "Current task",
"projectId": project_id
}
start_response = requests.post(start_url, headers=headers, json=start_payload)
return {
"today_hours": total_hours,
"entries_count": len(entries),
"new_timer": start_response.json()
}
```
### Weekly Report Generation
```python
def generate_weekly_report(workspace_id, week_start_date):
"""Generate weekly report for all projects"""
week_end = week_start_date + timedelta(days=6, hours=23, minutes=59, seconds=59)
url = f"{REPORTS_URL}/workspaces/{workspace_id}/reports/summary"
payload = {
"dateRangeStart": week_start_date.isoformat() + "Z",
"dateRangeEnd": week_end.isoformat() + "Z",
"summaryFilter": {
"groups": ["PROJECT", "USER"],
"sortColumn": "DURATION",
"sortOrder": "DESCENDING"
},
"amountShown": "EARNED",
"exportType": "JSON"
}
response = requests.post(url, headers=headers, json=payload)
data = response.json()
# Process data
report = []
for project in data.get('groupOne', []):
project_info = {
"name": project['name'],
"total_hours": project['duration'] / 3600,
"total_amount": project['amount'] / 100,
"users": []
}
for user in project.get('children', []):
project_info["users"].append({
"name": user['name'],
"hours": user['duration'] / 3600,
"amount": user['amount'] / 100
})
report.append(project_info)
return report
```
### Project Setup with Team
```python
def setup_project_with_team(workspace_id, project_name, client_id, team_config):
"""
Create project and assign team with rates
team_config = [
{"user_id": "user1", "rate": 15000},
{"user_id": "user2", "rate": 12000}
]
"""
# 1. Create project
project_url = f"{BASE_URL}/workspaces/{workspace_id}/projects"
project_payload = {
"name": project_name,
"clientId": client_id,
"isPublic": True,
"billable": True,
"color": "#0B83D9"
}
project_response = requests.post(project_url, headers=headers, json=project_payload)
project = project_response.json()
project_id = project['id']
# 2. Add team members with rates
team_url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/users"
memberships = [
{
"userId": member["user_id"],
"membershipStatus": "ACTIVE",
"membershipType": "PROJECT",
"hourlyRate": {
"amount": member["rate"],
"currency": "USD"
}
}
for member in team_config
]
team_payload = {"memberships": memberships}
requests.post(team_url, headers=headers, json=team_payload)
# 3. Set estimate
estimate_url = f"{BASE_URL}/workspaces/{workspace_id}/projects/{project_id}/estimate"
estimate_payload = {
"timeEstimate": {
"active": True,
"estimate": "PT200H",
"type": "AUTO",
"resetOption": "MONTHLY"
}
}
requests.patch(estimate_url, headers=headers, json=estimate_payload)
return project
```
---
## Error Handling Template
```python
def make_api_call(url, method='GET', payload=None, params=None):
"""Generic API call with error handling"""
try:
if method == 'GET':
response = requests.get(url, headers=headers, params=params)
elif method == 'POST':
response = requests.post(url, headers=headers, json=payload)
elif method == 'PUT':
response = requests.put(url, headers=headers, json=payload)
elif method == 'PATCH':
response = requests.patch(url, headers=headers, json=payload)
elif method == 'DELETE':
response = requests.delete(url, headers=headers)
response.raise_for_status()
if response.status_code == 204:
return None
return response.json()
except requests.exceptions.HTTPError as e:
if e.response.status_code == 401:
raise Exception("Invalid API key")
elif e.response.status_code == 403:
raise Exception("Insufficient permissions")
elif e.response.status_code == 404:
raise Exception("Resource not found")
elif e.response.status_code == 429:
raise Exception("Rate limit exceeded")
else:
raise Exception(f"API error: {e.response.text}")
except requests.exceptions.RequestException as e:
raise Exception(f"Request failed: {str(e)}")
```
---
## Notes
- All dates in UTC with 'Z' suffix
- Amounts in cents (smallest currency unit)
- Durations in ISO-8601 format (PT#H#M)
- Page sizes typically max at 1000
- Reports API uses different base URL
- Always handle pagination for large datasets
- Verify webhook signatures in production
- Cache reference data appropriately