everhour-integration
Complete Everhour time tracking integration for accessing and managing time entries via REST API
When & Why to Use This Skill
This Claude skill provides a comprehensive integration with the Everhour REST API, enabling automated time tracking, project management, and team reporting. It allows users to programmatically manage time entries, control timers, set task estimates, and synchronize data with Jira, significantly streamlining productivity workflows and financial budget monitoring.
Use Cases
- Automated Daily Reporting: Automatically generate and distribute detailed daily time logs and summaries for team members to improve operational transparency.
- Project Budget Monitoring: Track real-time project hours against predefined estimates to prevent budget overruns and optimize resource allocation.
- Seamless Jira Integration: Synchronize Everhour time entries with Jira issues to maintain consistent and accurate work logs across development and management platforms.
- Dynamic Timer Management: Programmatically start, stop, and switch timers for various tasks to ensure precise activity tracking without manual intervention.
- Task Estimate Optimization: Set and update task estimates via API to keep project timelines aligned with actual progress and team capacity.
| name | everhour-integration |
|---|---|
| version | 1.0 |
| description | Complete Everhour time tracking integration for accessing and managing time entries via REST API |
Everhour Integration Skill
Comprehensive time tracking integration using the Everhour REST API v1.
Overview
This skill provides complete programmatic access to Everhour time tracking data, including:
- User time entries and summaries
- Project and task time tracking
- Timer operations (start/stop)
- Task estimates and budgets
- Jira issue integration
- Team time reporting
API Base URL: https://api.everhour.com
Prerequisites
- Everhour Account: Active account with vividcg.atlassian.net
- API Token: Get from Everhour → Settings → My Profile → API Token
- Team Plan: Required for API access
- Python Dependencies:
requests,python-dotenv
Environment Configuration
Required Environment Variables:
EVERHOUR_API_TOKEN=your_api_token_here
EVERHOUR_BASE_URL=https://api.everhour.com
Core Functions
1. Authentication Helper
import os
from dotenv import load_dotenv
import requests
def get_everhour_auth():
"""Load Everhour credentials from environment."""
load_dotenv()
api_token = os.getenv('EVERHOUR_API_TOKEN')
if not api_token:
raise ValueError("EVERHOUR_API_TOKEN must be set in environment")
return {
'api_token': api_token,
'base_url': os.getenv('EVERHOUR_BASE_URL', 'https://api.everhour.com'),
'headers': {
'X-Api-Key': api_token,
'Content-Type': 'application/json'
}
}
2. Get Current User
def get_current_user():
"""Get current user information."""
auth = get_everhour_auth()
response = requests.get(
f"{auth['base_url']}/users/me",
headers=auth['headers']
)
response.raise_for_status()
user = response.json()
return {
'id': user.get('id'),
'name': user.get('name'),
'email': user.get('email'),
'status': user.get('status'),
'role': user.get('role'),
'team': user.get('team', {}).get('name')
}
3. Get Projects
def get_projects(platform_filter=None):
"""Get all projects, optionally filtered by platform.
Args:
platform_filter: Filter by platform (e.g., 'jira', 'asana', None for all)
Returns:
List of project dictionaries
"""
auth = get_everhour_auth()
response = requests.get(
f"{auth['base_url']}/projects",
headers=auth['headers']
)
response.raise_for_status()
projects = response.json()
if platform_filter:
# Note: Some projects don't have 'type' field
projects = [p for p in projects if p.get('type') == platform_filter]
return projects
4. Get Project Tasks
def get_project_tasks(project_id):
"""Get all tasks for a project.
Args:
project_id: Project ID (e.g., 'jr:6091-12165' for Jira projects)
Returns:
List of task dictionaries with time tracking data
"""
auth = get_everhour_auth()
response = requests.get(
f"{auth['base_url']}/projects/{project_id}/tasks",
headers=auth['headers']
)
response.raise_for_status()
return response.json()
5. Get Task Details
def get_task(task_id):
"""Get task details by ID or Jira issue key.
Args:
task_id: Everhour task ID (e.g., 'jr:6091-38923') or Jira key (e.g., 'DP01-74')
Returns:
Task dictionary with time tracking details
"""
auth = get_everhour_auth()
response = requests.get(
f"{auth['base_url']}/tasks/{task_id}",
headers=auth['headers']
)
if response.status_code == 404:
return None
response.raise_for_status()
task = response.json()
# Calculate total hours
total_ms = task.get('time', {}).get('total', 0)
total_hours = total_ms / 3600000
return {
'id': task.get('id'),
'name': task.get('name'),
'status': task.get('status'),
'total_time_ms': total_ms,
'total_hours': round(total_hours, 2),
'users': task.get('time', {}).get('users', {}),
'estimate_ms': task.get('estimate', 0),
'estimate_hours': round(task.get('estimate', 0) / 3600000, 2),
'projects': task.get('projects', [])
}
6. Get User Time Entries
from datetime import datetime, timedelta
def get_my_time_entries(start_date=None, end_date=None, days=30):
"""Get time entries for current user.
Args:
start_date: Start date (YYYY-MM-DD) or None for auto-calculate
end_date: End date (YYYY-MM-DD) or None for today
days: Number of days to look back if start_date is None
Returns:
List of time entry dictionaries
"""
auth = get_everhour_auth()
if end_date is None:
end_date = datetime.now().strftime("%Y-%m-%d")
if start_date is None:
start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
params = {
"from": start_date,
"to": end_date,
"limit": 1000
}
response = requests.get(
f"{auth['base_url']}/users/me/time",
headers=auth['headers'],
params=params
)
response.raise_for_status()
entries = response.json()
# Enrich with calculated hours
for entry in entries:
entry['hours'] = round(entry.get('time', 0) / 3600000, 2)
return entries
7. Add Time to Task
def add_time_to_task(task_id, hours, date=None, comment=None):
"""Add time entry to a task.
Args:
task_id: Everhour task ID or Jira issue key (e.g., 'DP01-74')
hours: Time in hours (converted to milliseconds)
date: Date in YYYY-MM-DD format (defaults to today)
comment: Optional description (max 1000 chars for Jira sync)
Returns:
Response from API
"""
auth = get_everhour_auth()
if date is None:
date = datetime.now().strftime("%Y-%m-%d")
time_ms = int(hours * 3600000)
payload = {
"time": time_ms,
"date": date
}
if comment:
payload["comment"] = comment[:1000] # Limit for Jira sync
response = requests.post(
f"{auth['base_url']}/tasks/{task_id}/time",
headers=auth['headers'],
json=payload
)
response.raise_for_status()
return response.json()
8. Update Time Entry
def update_time_entry(task_id, hours, date):
"""Update existing time entry for a task on a specific date.
Args:
task_id: Everhour task ID or Jira issue key
hours: New time in hours
date: Date in YYYY-MM-DD format
Returns:
Response from API
"""
auth = get_everhour_auth()
time_ms = int(hours * 3600000)
payload = {
"time": time_ms,
"date": date
}
response = requests.put(
f"{auth['base_url']}/tasks/{task_id}/time",
headers=auth['headers'],
json=payload
)
response.raise_for_status()
return response.json()
9. Delete Time Entry
def delete_time_entry(task_id, date):
"""Delete time entry for a task on a specific date.
Args:
task_id: Everhour task ID or Jira issue key
date: Date in YYYY-MM-DD format
Returns:
Response from API
"""
auth = get_everhour_auth()
payload = {"date": date}
response = requests.delete(
f"{auth['base_url']}/tasks/{task_id}/time",
headers=auth['headers'],
json=payload
)
response.raise_for_status()
return response.json()
10. Timer Operations
def start_timer(task_id):
"""Start timer for a task.
Args:
task_id: Everhour task ID or Jira issue key
Returns:
Timer response
"""
auth = get_everhour_auth()
payload = {"task": task_id}
response = requests.post(
f"{auth['base_url']}/timers",
headers=auth['headers'],
json=payload
)
response.raise_for_status()
return response.json()
def stop_timer():
"""Stop currently running timer.
Returns:
Timer response
"""
auth = get_everhour_auth()
payload = {"status": "stopped"}
response = requests.post(
f"{auth['base_url']}/timers/current",
headers=auth['headers'],
json=payload
)
response.raise_for_status()
return response.json()
def get_running_timer():
"""Get currently running timer.
Returns:
Timer response or None if no timer running
"""
auth = get_everhour_auth()
response = requests.get(
f"{auth['base_url']}/timers/current",
headers=auth['headers']
)
if response.status_code == 404:
return None
response.raise_for_status()
return response.json()
11. Task Estimates
def set_task_estimate(task_id, hours):
"""Set estimate for a task.
Args:
task_id: Everhour task ID or Jira issue key
hours: Estimated time in hours
Returns:
Response from API
"""
auth = get_everhour_auth()
time_ms = int(hours * 3600000)
payload = {"total": time_ms}
response = requests.post(
f"{auth['base_url']}/tasks/{task_id}/estimate",
headers=auth['headers'],
json=payload
)
response.raise_for_status()
return response.json()
def delete_task_estimate(task_id):
"""Delete estimate for a task.
Args:
task_id: Everhour task ID or Jira issue key
Returns:
Response from API
"""
auth = get_everhour_auth()
response = requests.delete(
f"{auth['base_url']}/tasks/{task_id}/estimate",
headers=auth['headers']
)
response.raise_for_status()
return response.json()
Jira Integration Specifics
Getting Jira Issue Time
def get_jira_issue_time(jira_key):
"""Get time tracking summary for a Jira issue.
Args:
jira_key: Jira issue key (e.g., 'DP01-74')
Returns:
Dictionary with time summary or None if issue not found
"""
task = get_task(jira_key)
if not task:
return None
return {
'jira_key': jira_key,
'task_name': task['name'],
'status': task['status'],
'total_hours': task['total_hours'],
'estimate_hours': task['estimate_hours'],
'users': task['users']
}
Sync Status
Important: Everhour → Jira sync is one-way:
- ✅ Time logged in Everhour appears in Jira work logs
- ❌ Native Jira work logs do NOT sync to Everhour
- ❌ Historical data before sync enabled is NOT synchronized
- ⚠️ Comments limited to 1000 characters for Jira sync
- ⚠️ Text formatting and emojis removed in sync
Common Use Cases
Use Case 1: Daily Time Report
from collections import defaultdict
def generate_daily_time_report(date=None):
"""Generate report of time entries for a specific date."""
if date is None:
date = datetime.now().strftime("%Y-%m-%d")
entries = get_my_time_entries(start_date=date, end_date=date)
report = {
'date': date,
'total_hours': 0,
'entries': []
}
for entry in entries:
hours = entry['hours']
report['total_hours'] += hours
# Get task details
task = get_task(entry['task'])
report['entries'].append({
'task_id': entry['task'],
'task_name': task['name'] if task else 'Unknown',
'hours': hours,
'comment': entry.get('comment', '')
})
return report
Use Case 2: Project Time Summary
def get_project_time_summary(project_id):
"""Get total time logged for all tasks in a project."""
tasks = get_project_tasks(project_id)
total_time_ms = 0
task_summaries = []
for task in tasks:
task_time = task.get('time', {}).get('total', 0)
total_time_ms += task_time
if task_time > 0: # Only include tasks with time
task_summaries.append({
'id': task.get('id'),
'name': task.get('name'),
'status': task.get('status'),
'hours': round(task_time / 3600000, 2)
})
# Sort by time descending
task_summaries.sort(key=lambda x: x['hours'], reverse=True)
return {
'project_id': project_id,
'total_hours': round(total_time_ms / 3600000, 2),
'task_count': len(task_summaries),
'tasks': task_summaries
}
Use Case 3: Budget Tracking Alert
def check_task_budget(task_id, budget_hours):
"""Check if a task is approaching budget limit."""
task = get_task(task_id)
if not task:
return None
logged_hours = task['total_hours']
estimated_hours = task['estimate_hours']
budget_remaining = budget_hours - logged_hours
percent_used = (logged_hours / budget_hours * 100) if budget_hours > 0 else 0
return {
'task_id': task_id,
'task_name': task['name'],
'logged_hours': logged_hours,
'estimated_hours': estimated_hours,
'budget_hours': budget_hours,
'budget_remaining': budget_remaining,
'percent_used': round(percent_used, 1),
'over_budget': logged_hours > budget_hours,
'approaching_budget': percent_used >= 80 and not logged_hours > budget_hours
}
Use Case 4: DP01 Project Time Tracking
def get_dp01_time_summary():
"""Get time tracking summary for DP01 - Datapage project."""
# DP01 project ID from Everhour
DP01_PROJECT_ID = "jr:6091-12165"
summary = get_project_time_summary(DP01_PROJECT_ID)
print(f"DP01 - Datapage Phase 1 Time Summary")
print(f"=" * 60)
print(f"Total Time: {summary['total_hours']} hours")
print(f"Tasks with Time: {summary['task_count']}")
print(f"\nTop 10 Tasks by Time:")
for task in summary['tasks'][:10]:
print(f" {task['hours']}h - {task['name']} ({task['status']})")
return summary
Integration with Jira Automation Skill
Combine with the Jira Automation skill for comprehensive tracking:
from jira_automation import get_jira_auth, get_jira_config
import requests
def get_combined_issue_data(jira_key):
"""Get combined data from Jira and Everhour for an issue."""
# Get Jira issue details
jira_config = get_jira_config()
jira_response = requests.get(
f"{jira_config['api_url']}/issue/{jira_key}",
auth=jira_config['auth']
)
jira_issue = jira_response.json()
# Get Everhour time tracking
everhour_time = get_jira_issue_time(jira_key)
# Combined data
return {
'jira_key': jira_key,
'summary': jira_issue['fields']['summary'],
'status': jira_issue['fields']['status']['name'],
'assignee': jira_issue['fields']['assignee']['emailAddress'] if jira_issue['fields']['assignee'] else None,
'everhour_total_hours': everhour_time['total_hours'] if everhour_time else 0,
'everhour_users': everhour_time['users'] if everhour_time else {},
'jira_time_spent_seconds': jira_issue['fields'].get('timetracking', {}).get('timeSpentSeconds', 0)
}
Rate Limits & Best Practices
- Rate Limits: Stay under 100 requests per minute
- Bulk Queries: Use
/projects/{id}/tasksinstead of individual task queries - Error Handling: Implement retry logic with exponential backoff for 429 errors
- Caching: Cache project and task lists to reduce API calls
- Permissions:
/team/timeendpoint requires admin permissions; use/users/me/timefor user-level access
Error Handling
def safe_api_call(func, *args, **kwargs):
"""Wrapper for API calls with error handling."""
try:
return func(*args, **kwargs)
except requests.exceptions.HTTPError as e:
if e.response.status_code == 403:
print(f"[ERROR] Access denied - check permissions")
elif e.response.status_code == 404:
print(f"[ERROR] Resource not found")
elif e.response.status_code == 429:
print(f"[ERROR] Rate limit exceeded - retry later")
else:
print(f"[ERROR] HTTP {e.response.status_code}: {e.response.text}")
return None
except requests.exceptions.RequestException as e:
print(f"[ERROR] Network error: {e}")
return None
Testing
Test scripts available in /scripts/:
test-everhour-api.py- Basic API connectivity testfind-dp01-project.py- Find DP01 project and list tasksget-my-time-entries.py- Get your time entries
Known Limitations
- Historical Data: Pre-integration time entries are not synced
- One-Way Sync: Native Jira work logs don't flow back to Everhour
- Comment Length: Max 1,000 characters for Jira sync
- No Formatting: Text formatting and emojis removed in sync
- Paid Plan Required: API access requires Everhour Team plan
- Admin Endpoints:
/team/timerequires admin role
Related Documentation
- EVERHOUR_API_INTEGRATION_GUIDE.md - Complete API reference
- jira-automation - Jira REST API integration
- Official Everhour API Docs
Version History
- 1.0 (2025-12-15): Initial release with core API functions
Maintained By: Connect 2.0 Platform Development Team Last Updated: December 15, 2025