# How to Integrate Replit with Clockify

- Tool: Replit
- Difficulty: Intermediate
- Time required: 25 minutes
- Last updated: March 2026

## TL;DR

To integrate Replit with Clockify, generate a Clockify API key from your profile settings, store it in Replit Secrets (lock icon 🔒), and use the Clockify API from Python or Node.js server-side code to create time entries, manage projects, and pull time reports. Use Autoscale deployment for web apps that record time from a custom interface.

## Why Connect Replit to Clockify?

Clockify offers a generous free tier that supports unlimited users and workspaces, making it one of the most accessible time tracking APIs for developers. By connecting your Replit app to Clockify, you can automate time entry logging, build custom reporting dashboards, sync time data with project management tools, and create internal tools that track billable hours without requiring your team to manually switch to the Clockify interface.

Common integration patterns include automatically starting a time entry when a user begins a task in your app, logging completed task durations to Clockify when a ticket is closed in a project management tool, and pulling time reports to generate client invoices. The Clockify API v1 is RESTful, well-documented, and uses simple API key authentication — making it one of the easier time tracking APIs to work with in a Replit environment.

Replit's Secrets system (lock icon 🔒 in the sidebar) keeps your Clockify API key secure and out of your codebase. The API key has the same permissions as the account that generated it, so treat it like a password. Store it in Replit Secrets and access it via os.environ['CLOCKIFY_API_KEY'] in Python or process.env.CLOCKIFY_API_KEY in Node.js — never paste it directly into code files.

## Before you start

- A Replit account with a Python or Node.js project created
- A Clockify account (free tier available with full API access)
- At least one workspace and one project created in Clockify
- Basic familiarity with REST APIs and HTTP headers
- Node.js 18+ or Python 3.10+ (both available on Replit by default)

## Step-by-step guide

### 1. Generate a Clockify API Key and Find Your Workspace ID

Log in to your Clockify account and click your avatar or initials in the top-right corner. Select 'Profile Settings' from the dropdown menu. Scroll to the very bottom of the Profile Settings page to find the 'API' section. Click 'Generate' to create a new API key. The key is displayed once — copy it immediately before navigating away.

Your Workspace ID is required for nearly all Clockify API endpoints. The easiest way to find it is to make a test API call to https://api.clockify.me/api/v1/workspaces using your API key. The response returns an array of workspace objects each containing an 'id' field — this is your Workspace ID. Alternatively, you can find it in the Clockify web app URL when you are viewing a workspace: it appears as the segment after '/workspaces/' in the URL.

Copy both the API key and the Workspace ID. You will also want to note the IDs of any projects you want to log time against. In Clockify, navigate to Projects, click a project, and the project ID appears in the URL. You can also fetch all project IDs programmatically from /workspaces/{workspaceId}/projects once your integration is running.

**Expected result:** You have your Clockify API key and Workspace ID copied and ready to store in Replit Secrets.

### 2. Store Clockify Credentials in Replit Secrets

Open your Replit project and click the lock icon 🔒 in the left sidebar to open the Secrets pane. Add the following secrets using the 'Add a new secret' form:

- Key: CLOCKIFY_API_KEY — Value: your generated API key
- Key: CLOCKIFY_WORKSPACE_ID — Value: your Workspace ID

Click 'Add Secret' after each one. Replit encrypts these values at rest and injects them as environment variables at runtime, keeping them out of your code files and Git history entirely.

In Python, read these with os.environ['CLOCKIFY_API_KEY'] and os.environ['CLOCKIFY_WORKSPACE_ID']. In Node.js, use process.env.CLOCKIFY_API_KEY and process.env.CLOCKIFY_WORKSPACE_ID. Replit's built-in Secret Scanner will warn you if you accidentally type an API key directly into a code file.

If you work with multiple Clockify workspaces, add a separate Secret for each: CLOCKIFY_WORKSPACE_DEVELOPMENT_ID, CLOCKIFY_WORKSPACE_CLIENT_ID, etc. This is cleaner than switching workspace IDs in code.

**Expected result:** Two Secrets (CLOCKIFY_API_KEY and CLOCKIFY_WORKSPACE_ID) appear in the Replit Secrets pane.

### 3. Create and Manage Time Entries with Python

The Clockify API uses a custom header X-Api-Key for authentication instead of the more common Authorization Bearer header. All requests to workspace-specific resources include the Workspace ID in the URL path. The base URL is https://api.clockify.me/api/v1/.

Time entries in Clockify require a start time in ISO 8601 format with timezone offset. If you are creating an in-progress entry (timer running), omit the end time. If you are logging a completed entry (timesheet mode), provide both start and end. Durations in Clockify reports are expressed in ISO 8601 duration format (e.g., 'PT1H30M' for 1 hour 30 minutes).

The code below provides a complete Python client covering the most common operations: listing projects, starting and stopping time entries, creating completed entries, and fetching time reports. Install the requests library if it is not already available (pip install requests).

```
import os
import requests
from datetime import datetime, timezone, timedelta

API_KEY = os.environ["CLOCKIFY_API_KEY"]
WORKSPACE_ID = os.environ["CLOCKIFY_WORKSPACE_ID"]
BASE_URL = "https://api.clockify.me/api/v1"

# Clockify uses X-Api-Key header (not Authorization Bearer)
HEADERS = {
    "X-Api-Key": API_KEY,
    "Content-Type": "application/json"
}

def get_workspaces() -> list:
    """List all workspaces accessible with this API key."""
    response = requests.get(f"{BASE_URL}/workspaces", headers=HEADERS)
    response.raise_for_status()
    return response.json()

def get_projects() -> list:
    """List all projects in the workspace."""
    url = f"{BASE_URL}/workspaces/{WORKSPACE_ID}/projects"
    response = requests.get(url, headers=HEADERS, params={"page-size": 200})
    response.raise_for_status()
    return response.json()

def get_current_user() -> dict:
    """Get the current authenticated user's info (includes userId)."""
    response = requests.get(f"{BASE_URL}/user", headers=HEADERS)
    response.raise_for_status()
    return response.json()

def create_time_entry(
    start: datetime,
    end: datetime,
    project_id: str,
    description: str = "",
    billable: bool = True
) -> dict:
    """
    Create a completed time entry.
    start and end should be timezone-aware datetime objects.
    """
    data = {
        "start": start.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "end": end.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "projectId": project_id,
        "description": description,
        "billable": billable
    }
    url = f"{BASE_URL}/workspaces/{WORKSPACE_ID}/time-entries"
    response = requests.post(url, json=data, headers=HEADERS)
    response.raise_for_status()
    return response.json()

def start_timer(project_id: str, description: str = "") -> dict:
    """Start a running timer (no end time — Clockify tracks until stopped)."""
    now = datetime.now(timezone.utc)
    data = {
        "start": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "projectId": project_id,
        "description": description
    }
    url = f"{BASE_URL}/workspaces/{WORKSPACE_ID}/time-entries"
    response = requests.post(url, json=data, headers=HEADERS)
    response.raise_for_status()
    return response.json()

def stop_timer(user_id: str) -> dict:
    """Stop the currently running timer for a user."""
    now = datetime.now(timezone.utc)
    url = f"{BASE_URL}/workspaces/{WORKSPACE_ID}/user/{user_id}/time-entries"
    data = {"end": now.strftime("%Y-%m-%dT%H:%M:%SZ")}
    response = requests.patch(url, json=data, headers=HEADERS)
    response.raise_for_status()
    return response.json()

def get_time_entries(user_id: str, start: datetime, end: datetime) -> list:
    """Fetch time entries for a user within a date range."""
    url = f"{BASE_URL}/workspaces/{WORKSPACE_ID}/user/{user_id}/time-entries"
    params = {
        "start": start.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "end": end.strftime("%Y-%m-%dT%H:%M:%SZ"),
        "page-size": 500
    }
    response = requests.get(url, params=params, headers=HEADERS)
    response.raise_for_status()
    return response.json()

# Example usage
if __name__ == "__main__":
    user = get_current_user()
    print(f"Connected as: {user['name']} ({user['email']})")

    projects = get_projects()
    print(f"Projects: {[p['name'] for p in projects[:5]]}")

    if projects:
        # Log a 2-hour entry for the first project
        end_time = datetime.now(timezone.utc)
        start_time = end_time - timedelta(hours=2)
        entry = create_time_entry(start_time, end_time, projects[0]['id'], "Example task")
        print(f"Time entry created: {entry['id']}")
```

**Expected result:** Running the script prints your Clockify username, lists up to 5 projects, and creates a 2-hour time entry on the first project.

### 4. Build a Node.js Time Tracking API

For Node.js projects, use the axios package (npm install axios) to call the Clockify API. The Express server below provides REST endpoints that your frontend can call to start and stop timers, list projects, and view time entries. All Clockify API calls are handled server-side to protect your API key.

Note that Clockify requires the user ID for all user-specific operations like starting/stopping timers and fetching time entries. The user ID is available from the GET /user endpoint using your API key. In a multi-user app, each user would have their own Clockify API key stored in your database; for a single-admin integration, you can retrieve the user ID once and store it as CLOCKIFY_USER_ID in Replit Secrets.

The server uses an axios instance pre-configured with the API key header so you do not need to add it to every request individually.

```
const express = require('express');
const axios = require('axios');

const app = express();
app.use(express.json());

const API_KEY = process.env.CLOCKIFY_API_KEY;
const WORKSPACE_ID = process.env.CLOCKIFY_WORKSPACE_ID;
const BASE_URL = 'https://api.clockify.me/api/v1';

// Axios instance with Clockify API key header
const clockify = axios.create({
  baseURL: BASE_URL,
  headers: { 'X-Api-Key': API_KEY }
});

// Get current user (useful for retrieving userId)
app.get('/user', async (req, res) => {
  try {
    const response = await clockify.get('/user');
    res.json(response.data);
  } catch (err) {
    res.status(err.response?.status || 500).json({ error: err.message });
  }
});

// List projects in the workspace
app.get('/projects', async (req, res) => {
  try {
    const response = await clockify.get(
      `/workspaces/${WORKSPACE_ID}/projects`,
      { params: { 'page-size': 200 } }
    );
    res.json(response.data);
  } catch (err) {
    res.status(err.response?.status || 500).json({ error: err.message });
  }
});

// Create a completed time entry
app.post('/time-entries', async (req, res) => {
  const { start, end, projectId, description, billable } = req.body;
  if (!start || !end || !projectId) {
    return res.status(400).json({ error: 'start, end, and projectId are required' });
  }
  try {
    const response = await clockify.post(
      `/workspaces/${WORKSPACE_ID}/time-entries`,
      { start, end, projectId, description: description || '', billable: billable !== false }
    );
    res.status(201).json(response.data);
  } catch (err) {
    console.error('Time entry error:', err.response?.data);
    res.status(err.response?.status || 500).json({ error: err.message });
  }
});

// Get time entries for a user within a date range
// Query params: userId, start (ISO), end (ISO)
app.get('/time-entries', async (req, res) => {
  const { userId, start, end } = req.query;
  if (!userId || !start || !end) {
    return res.status(400).json({ error: 'userId, start, and end are required' });
  }
  try {
    const response = await clockify.get(
      `/workspaces/${WORKSPACE_ID}/user/${userId}/time-entries`,
      { params: { start, end, 'page-size': 500 } }
    );
    res.json(response.data);
  } catch (err) {
    res.status(err.response?.status || 500).json({ error: err.message });
  }
});

// Stop running timer for a user
app.post('/timers/stop', async (req, res) => {
  const { userId } = req.body;
  if (!userId) return res.status(400).json({ error: 'userId is required' });
  const end = new Date().toISOString().replace('.000', '');
  try {
    const response = await clockify.patch(
      `/workspaces/${WORKSPACE_ID}/user/${userId}/time-entries`,
      { end }
    );
    res.json(response.data);
  } catch (err) {
    res.status(err.response?.status || 500).json({ error: err.message });
  }
});

app.listen(3000, '0.0.0.0', () => {
  console.log('Clockify integration server running on port 3000');
});
```

**Expected result:** The server starts on port 3000. A GET request to /user returns your Clockify profile JSON, and a GET to /projects returns the list of projects in your workspace.

### 5. Generate Time Reports and Deploy

The Clockify Reports API (at api.clockify.me/api/v1/workspaces/{workspaceId}/reports/) provides pre-aggregated summary and detailed reports that are more efficient than fetching raw time entries. The Summary report groups time by user, project, or client and returns totals with duration in seconds.

For deployment, choose Autoscale in Replit if your app serves a web frontend and processes time entry submissions from users. Autoscale handles variable traffic and scales to zero when idle, keeping costs low. Choose Reserved VM if your app needs to run a continuous background process — for example, a timer sync service that polls an external project management tool and auto-creates Clockify entries.

To access deployment options, click 'Deploy' in the Replit toolbar. Your deployed app gets a stable URL at https://your-app.replit.app, which is required if you want to use Clockify webhook callbacks (available on paid Clockify plans) to receive notifications when time entries are updated.

```
import os
import requests
from datetime import datetime, timezone, timedelta

API_KEY = os.environ["CLOCKIFY_API_KEY"]
WORKSPACE_ID = os.environ["CLOCKIFY_WORKSPACE_ID"]
HEADERS = {"X-Api-Key": API_KEY, "Content-Type": "application/json"}
REPORTS_URL = f"https://api.clockify.me/api/v1/workspaces/{WORKSPACE_ID}/reports"

def get_summary_report(start_date: datetime, end_date: datetime) -> dict:
    """
    Get a summary report grouped by project.
    Returns total hours per project for the date range.
    """
    payload = {
        "dateRangeStart": start_date.strftime("%Y-%m-%dT00:00:00Z"),
        "dateRangeEnd": end_date.strftime("%Y-%m-%dT23:59:59Z"),
        "summaryFilter": {
            "groups": ["PROJECT", "USER"]
        },
        "exportType": "JSON"
    }
    response = requests.post(f"{REPORTS_URL}/summary", json=payload, headers=HEADERS)
    response.raise_for_status()
    return response.json()

def format_duration(seconds: int) -> str:
    """Convert seconds to hours and minutes string."""
    hours = seconds // 3600
    minutes = (seconds % 3600) // 60
    return f"{hours}h {minutes}m"

if __name__ == "__main__":
    # Generate weekly report
    end = datetime.now(timezone.utc)
    start = end - timedelta(days=7)

    print(f"Time report: {start.date()} to {end.date()}\n")
    report = get_summary_report(start, end)

    for group in report.get('groupOne', []):
        project_name = group.get('name', 'No Project')
        total_seconds = group.get('duration', 0)
        print(f"  {project_name}: {format_duration(total_seconds)}")

        # Show per-user breakdown
        for child in group.get('children', []):
            user_name = child.get('name', 'Unknown')
            user_seconds = child.get('duration', 0)
            print(f"    - {user_name}: {format_duration(user_seconds)}")

    total = report.get('totals', [{}])[0].get('totalTime', 0)
    print(f"\nTotal: {format_duration(total)}")
```

**Expected result:** Running the script prints a formatted weekly report showing total hours per project and per user for the past 7 days.

## Best practices

- Always store CLOCKIFY_API_KEY and CLOCKIFY_WORKSPACE_ID in Replit Secrets (lock icon 🔒) — never hardcode them in source files.
- Retrieve and cache your Clockify user ID once at startup using GET /user, rather than calling it on every request — the user ID does not change.
- Format all timestamps as UTC with the Z suffix (YYYY-MM-DDTHH:MM:SSZ) — Clockify rejects local timezone offsets in most endpoints.
- Use the Reports API for aggregated data when on a paid Clockify plan — it is significantly faster than fetching and aggregating raw time entries.
- Implement pagination for project and time entry lists by using page and page-size query parameters — workspaces with many entries will not return all records in a single request.
- Deploy with Autoscale for web apps that accept time entries from users; use Reserved VM for background scripts that sync time data on a schedule.
- Use billable: true on time entries for client-facing projects so they are included in Clockify's billing reports and invoice exports.
- Test your integration against a separate Clockify workspace rather than your production workspace to avoid polluting real time tracking data during development.

## Use cases

### Automatic Time Logging from a Task Management App

When a user marks a task as complete in your Replit app, automatically create a Clockify time entry for the duration spent on that task. The integration reads the task start and end timestamps from your database and posts a corresponding time entry to the correct Clockify project, eliminating manual time logging entirely.

Prompt example:

```
Build a Flask endpoint that accepts a task completion event with start_time, end_time, project_id, and user_id, then creates a Clockify time entry for the calculated duration using CLOCKIFY_API_KEY and CLOCKIFY_WORKSPACE_ID from Replit Secrets.
```

### Weekly Time Report Generator

Build a scheduled Replit script that pulls all time entries for the past week from Clockify, groups them by project and user, and generates a summary report. The report can be emailed to stakeholders, posted to Slack, or written to a Google Sheet, giving managers visibility into team productivity without logging into Clockify.

Prompt example:

```
Write a Python script that fetches all Clockify time entries for the current workspace for the past 7 days, groups hours by project name, calculates total billable hours per project, and outputs a formatted summary report showing project totals and top contributors.
```

### Client Billing Dashboard

Create a Replit web app that pulls Clockify time data for a specific client project, calculates billable amounts based on hourly rates, and displays an invoice-ready summary. This replaces the manual process of exporting Clockify reports and formatting them in a spreadsheet.

Prompt example:

```
Build an Express server with a GET /billing/:projectId endpoint that fetches all time entries for a Clockify project, multiplies each entry's duration by a configurable hourly rate stored in the database, and returns a billing summary with total hours and amount due.
```

## Troubleshooting

### API returns 401 Unauthorized with message 'Full authentication is required'

Cause: The X-Api-Key header is missing, incorrectly named, or contains an invalid/revoked API key. Clockify does not use the standard Authorization header — it requires X-Api-Key specifically.

Solution: Check that your request includes the header X-Api-Key (not Authorization or X-API-Key). Verify the key value in Replit Secrets matches the one shown in Clockify Profile Settings > API. If you recently regenerated the key in Clockify, update the CLOCKIFY_API_KEY Secret in Replit.

```
# Python: correct header name
HEADERS = {
    "X-Api-Key": os.environ["CLOCKIFY_API_KEY"],  # Must be exactly 'X-Api-Key'
    "Content-Type": "application/json"
}
```

### POST /time-entries returns 400 with 'Start or end time is null or invalid'

Cause: The datetime strings are not in the correct ISO 8601 UTC format that Clockify requires. Common mistakes include missing the Z suffix, including milliseconds, or using a local timezone offset instead of UTC.

Solution: Format all datetimes as YYYY-MM-DDTHH:MM:SSZ (UTC, no milliseconds). In Python, use datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'). In Node.js, use new Date().toISOString().replace('.000Z', 'Z') or slice(0, 19) + 'Z'.

```
// Node.js: correct UTC format for Clockify
const start = new Date(startTime).toISOString().slice(0, 19) + 'Z';
const end = new Date(endTime).toISOString().slice(0, 19) + 'Z';
```

### GET /workspaces/{id}/projects returns empty array despite having projects in Clockify

Cause: The CLOCKIFY_WORKSPACE_ID in Replit Secrets does not match any workspace accessible to the API key, or the workspace ID is from the wrong Clockify account.

Solution: Call GET /workspaces first to retrieve the correct workspace ID for the account associated with your API key. The workspace ID is the 'id' field in the response array. Update CLOCKIFY_WORKSPACE_ID in Replit Secrets with the value from this response.

```
# Python: fetch the correct workspace ID
response = requests.get("https://api.clockify.me/api/v1/workspaces", headers=HEADERS)
workspaces = response.json()
for ws in workspaces:
    print(f"Workspace: {ws['name']} — ID: {ws['id']}")
```

### Reports API returns 403 Forbidden

Cause: The Clockify Reports API (at /reports/) is only available on Standard plan and above. Free tier accounts can only access the basic time entries API, not the aggregated reports endpoints.

Solution: Upgrade to Clockify's Standard plan for access to the Reports API. As an alternative on the free tier, fetch raw time entries from /workspaces/{id}/user/{userId}/time-entries and aggregate them manually in your Python or Node.js code.

```
# Free tier alternative: manually aggregate time entries
def get_project_totals(entries: list) -> dict:
    totals = {}
    for entry in entries:
        project = entry.get('project', {}).get('name', 'No Project')
        duration = entry.get('timeInterval', {}).get('duration', 'PT0S')
        # Parse ISO 8601 duration (PT1H30M = 5400 seconds)
        totals[project] = totals.get(project, 0) + parse_duration(duration)
    return totals
```

## Frequently asked questions

### How do I store my Clockify API key in Replit?

Click the lock icon 🔒 in the left sidebar of your Replit project to open the Secrets pane. Add CLOCKIFY_API_KEY with your API key from Clockify Profile Settings > API. Also add CLOCKIFY_WORKSPACE_ID with your workspace ID. Access them in Python with os.environ['CLOCKIFY_API_KEY'] and in Node.js with process.env.CLOCKIFY_API_KEY.

### Can I use the Clockify API on the free tier?

Yes. Clockify's free tier includes full API access for basic operations: creating time entries, listing projects, and fetching raw time entry data. The Reports API (aggregated summaries) requires the Standard plan or above. All other endpoints work on the free tier with no rate limit restrictions for normal usage.

### How do I find my Clockify Workspace ID?

Call GET https://api.clockify.me/api/v1/workspaces with your X-Api-Key header. The response returns an array of workspace objects — the 'id' field in each object is the Workspace ID. Copy this value and store it as CLOCKIFY_WORKSPACE_ID in Replit Secrets. It also appears in the Clockify web app URL as the segment after /workspaces/.

### What datetime format does Clockify require?

Clockify requires ISO 8601 UTC format: YYYY-MM-DDTHH:MM:SSZ. The Z suffix means UTC. Do not include milliseconds (.000) and do not use local timezone offsets (+05:00). In Python, use datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'). In Node.js, use new Date().toISOString().slice(0, 19) + 'Z'.

### Does Replit work with Clockify webhooks?

Clockify webhooks are available on the Standard plan and above. Once you have a deployed Replit app (using Autoscale or Reserved VM), register your webhook URL in Clockify under Workspace Settings > Integrations > Webhooks. Clockify sends POST requests with JSON payloads when time entries are created, updated, or deleted.

### How do I handle multiple users logging time to the same Clockify workspace?

In Clockify, each user has their own account with their own API key. For a multi-user integration, you can either: (1) invite users to your workspace and use your own API key with admin permissions to create entries on their behalf by including their userId in requests, or (2) collect each user's API key during onboarding and store it encrypted in your database to make API calls as that user.

---

Source: https://www.rapidevelopers.com/replit-integration/clockify
© RapidDev — https://www.rapidevelopers.com/replit-integration/clockify
