# How to Integrate Lovable with Clockify

- Tool: Lovable
- Difficulty: Intermediate
- Time required: 35 minutes
- Last updated: March 2026

## TL;DR

Integrating Clockify with Lovable requires Edge Functions to proxy the Clockify REST API. Store your Clockify API key in Cloud Secrets, create an Edge Function that reads workspaces, projects, and time entries and starts or stops timers, and build custom time tracking interfaces in Lovable's React frontend. Clockify offers a generous free tier with unlimited users, making it ideal for teams who want professional time tracking without per-seat costs — and its clean API is one of the easiest to work with.

## Why integrate Clockify with Lovable?

Clockify has become the default free time tracker for freelancers, small agencies, and distributed teams precisely because it removes the biggest friction point of most tools: unlimited users at no cost. The free tier includes full time tracking, project management, reporting, and team features — paid tiers add scheduling, capacity management, and advanced invoicing. Over 5 million users track time in Clockify, and for many teams it serves as the single source of truth for hours worked.

Building a custom Lovable integration with Clockify is particularly useful for teams that want time tracking embedded in their own tools rather than in a separate Clockify tab: an operations dashboard that shows real-time project hours alongside task completion rates and budget data, a project management tool that displays time tracked against each task from Clockify without requiring a context switch, a client portal that shows verified hours logged to client projects, or a team capacity view that combines Clockify's time data with calendar availability.

Clockify's REST API v1 is well-documented and uses the X-Api-Key header for authentication — a simple, reliable scheme. The API is workspace-scoped: most endpoints require a workspace ID as a path component, and your API key must be a member of that workspace. Time entries are the core resource: each entry has a start time, end time, description, project association, and optional task association. The timer is a special resource — the currently running time entry for a user. This tutorial covers all the essential patterns from API setup through building production-ready timesheet and timer components.

## Before you start

- A Lovable project with Cloud enabled
- A Clockify account (free tier is sufficient for API access)
- A Clockify API key — generate at clockify.me/user/settings → API → Generate API key
- Your Clockify workspace ID — found in the URL when logged into Clockify (clockify.me/workspaces/WORKSPACE_ID) or via the API
- Project IDs for the projects you want to track — found in Clockify URLs or via GET /workspaces/{workspaceId}/projects

## Step-by-step guide

### 1. Generate your Clockify API key and store credentials in Cloud Secrets

Clockify API keys are personal credentials tied to your user account — they provide access to all workspaces you're a member of. To generate one, log into Clockify at clockify.me and click your profile photo in the top-right corner. Select 'Profile Settings', then scroll to the 'API' section at the bottom of the page. Click 'Generate' to create an API key (or 'Regenerate' if one already exists). Copy the key — it won't be shown again after you navigate away, though you can regenerate if needed.

You also need your workspace ID. The easiest way to find it is from your browser URL when using Clockify — it's the long string in clockify.me/workspaces/WORKSPACE_ID/. Alternatively, you can retrieve it via the API after setting up your credentials. In your Lovable project, open the Cloud tab by clicking '+' at the top of the screen, navigate to Secrets, and click 'Add secret'. Add two secrets: CLOCKIFY_API_KEY with your API key, and CLOCKIFY_WORKSPACE_ID with your workspace ID. Like Everhour, Clockify uses a custom X-Api-Key header rather than Bearer token — your Edge Function will set this header on every request. Clockify is notably one of the only major time tracking platforms with a fully functional API on the free tier.

**Expected result:** CLOCKIFY_API_KEY and CLOCKIFY_WORKSPACE_ID are stored in Cloud Secrets with masked values.

### 2. Create the Clockify proxy Edge Function

Clockify's API v1 is hosted at api.clockify.me/api/v1 and follows RESTful patterns with workspace-scoped endpoints. All project and time entry operations require the workspace ID in the path. The timer is managed via the /workspaces/{workspaceId}/user/{userId}/time-entries endpoint — to start a timer, POST a new time entry without an end time; to stop it, PATCH the current running entry with an end time.

The Edge Function below implements an action-based router covering the most common Clockify operations. Note the time entry endpoint structure: listing time entries uses GET /workspaces/{workspaceId}/user/{userId}/time-entries (user-scoped), while creating a timer uses POST to the same endpoint, and the reports endpoint uses a different base URL (reports.api.clockify.me) for detailed aggregated reporting. The function handles both API bases.

```
// supabase/functions/clockify-proxy/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";

const BASE = "https://api.clockify.me/api/v1";
const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};

serve(async (req) => {
  if (req.method === "OPTIONS") {
    return new Response("ok", { headers: corsHeaders });
  }
  const apiKey = Deno.env.get("CLOCKIFY_API_KEY");
  const workspaceId = Deno.env.get("CLOCKIFY_WORKSPACE_ID");
  if (!apiKey || !workspaceId) {
    return new Response(JSON.stringify({ error: "Clockify credentials not configured" }), {
      status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" },
    });
  }
  const authHeaders = { "X-Api-Key": apiKey, "Content-Type": "application/json" };
  const ws = workspaceId;
  const { action, params = {} } = await req.json();
  let resp;
  const now = new Date().toISOString();

  switch (action) {
    case "get_workspace":
      resp = await fetch(`${BASE}/workspaces/${ws}`, { headers: authHeaders });
      break;
    case "list_projects": {
      const q = new URLSearchParams({ archived: String(params.archived ?? false) });
      resp = await fetch(`${BASE}/workspaces/${ws}/projects?${q}`, { headers: authHeaders });
      break;
    }
    case "list_users":
      resp = await fetch(`${BASE}/workspaces/${ws}/users`, { headers: authHeaders });
      break;
    case "list_time_entries": {
      const q = new URLSearchParams();
      if (params.start) q.set("start", params.start);
      if (params.end) q.set("end", params.end);
      q.set("page-size", String(params.pageSize || 100));
      resp = await fetch(`${BASE}/workspaces/${ws}/user/${params.userId}/time-entries?${q}`, { headers: authHeaders });
      break;
    }
    case "get_current_timer":
      resp = await fetch(`${BASE}/workspaces/${ws}/user/${params.userId}/time-entries?in-progress=true`, { headers: authHeaders });
      break;
    case "start_timer":
      resp = await fetch(`${BASE}/workspaces/${ws}/time-entries`, {
        method: "POST", headers: authHeaders,
        body: JSON.stringify({
          start: now,
          projectId: params.projectId,
          description: params.description || "",
          billable: params.billable ?? true,
        }),
      });
      break;
    case "stop_timer":
      resp = await fetch(`${BASE}/workspaces/${ws}/user/${params.userId}/time-entries`, {
        method: "PATCH", headers: authHeaders,
        body: JSON.stringify({ end: now }),
      });
      break;
    case "add_time_entry":
      resp = await fetch(`${BASE}/workspaces/${ws}/time-entries`, {
        method: "POST", headers: authHeaders,
        body: JSON.stringify({
          start: params.start, end: params.end,
          projectId: params.projectId,
          description: params.description || "",
          billable: params.billable ?? true,
        }),
      });
      break;
    default:
      return new Response(JSON.stringify({ error: `Unknown action: ${action}` }), {
        status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" },
      });
  }

  const data = await resp.json();
  return new Response(JSON.stringify(data), {
    status: resp.status, headers: { ...corsHeaders, "Content-Type": "application/json" },
  });
});
```

**Expected result:** The Edge Function deploys. Calling action 'list_projects' returns your Clockify projects. Calling 'list_users' returns all workspace members with their user IDs.

### 3. Build a timer widget and time entry display

The timer component is the centerpiece of most Clockify integrations — it allows users to start and stop time tracking without leaving your Lovable app. The key UI elements are: a running timer display (showing elapsed time), a project selector, and start/stop controls. The elapsed time is calculated client-side from the timer's start time rather than making API calls every second, keeping the API call count low while maintaining accuracy.

On component mount, call get_current_timer to check if a timer is already running. If one is, display it immediately with the elapsed time counted from its start time. This handles the case where a user started tracking in Clockify's native app or another interface and now opens your Lovable app — the integration stays synchronized. Below the timer, a time entries list shows the day's logged entries with project names, descriptions, and durations.

**Expected result:** The timer widget shows the correct running/stopped state on load. Start/stop controls create and close time entries in Clockify. The daily entries list shows all entries for today with accurate durations.

### 4. Build a team timesheet view and reporting

Beyond personal timer control, Clockify shines for team timesheet management. Fetching time entries for all workspace users across a date range gives you the raw data to build any aggregated view: weekly timesheets, project hour summaries, billable vs non-billable breakdowns, and utilization reports. The time entries include a billable boolean, project ID, task ID, and duration in ISO 8601 duration format (e.g., PT2H30M for 2 hours 30 minutes).

For larger teams or longer date ranges, consider caching time entry data in Supabase to avoid re-fetching on every page load. Store a daily snapshot at end-of-day and use the cached data for historical views, only fetching live data for the current day. RapidDev's team can help design this caching layer for teams with complex reporting requirements across many projects and users.

**Expected result:** The timesheet renders all team members with correct hour totals per day. Color coding accurately reflects hours logged. The By Project toggle pivots the view. CSV export downloads the complete timesheet.

## Best practices

- Store your Clockify workspace ID as a Cloud Secret (CLOCKIFY_WORKSPACE_ID) rather than hardcoding it in Edge Function code — this makes it easy to switch between a test and production workspace.
- Use UTC ISO timestamps consistently for start and end times in all time entry operations — Clockify stores everything in UTC and client-side timezone handling causes date range mismatches.
- Parse ISO 8601 duration strings (PT2H30M) into decimal hours for calculations — avoid relying on the formatted duration display strings which vary by plan and settings.
- Implement page-size=200 in all list_time_entries calls to reduce the likelihood of hitting pagination limits for active teams, and add pagination handling for periods with very high entry counts.
- Cache workspace user lists with a 30-minute TTL — team composition changes infrequently but checking on every render is wasteful for teams with many members.
- For the timer UI, calculate elapsed time client-side using the entry's start timestamp and setInterval rather than polling the API — this keeps the display smooth at zero additional API cost.
- Handle the billable flag explicitly when creating time entries — Clockify defaults may or may not set entries as billable depending on project settings, so always pass billable: true or false explicitly based on your business rules.

## Use cases

### Embedded timer and time log in a project management tool

A team uses a custom Lovable project tracker with Supabase and wants to add Clockify time tracking directly — clicking a task starts the Clockify timer for the associated project, and clicking again stops it and logs the entry. A per-task time summary shows total hours logged across all team members without leaving the task management interface.

Prompt example:

```
Add Clockify time tracking to the task list component. Each task row shows a play/stop button and a today's hours badge. Clicking play for a task calls my Clockify Edge Function to start a timer for that task's associated Clockify project ID (stored in tasks.clockify_project_id). Show a live elapsed time counter on the active task row updating every second. Clicking stop logs the entry. Below the task title show 'Logged today: Xh' by fetching time entries for that project today. Retrieve the current timer state on component mount to restore the play/stop state if a timer was already running.
```

### Team timesheet dashboard for weekly review

A project manager wants a weekly view of all team members' logged hours, broken down by project, to compare against estimates and identify over-allocated team members. The Edge Function fetches time entries for all workspace users for the selected week, and the frontend renders a pivot table showing hours per person per project with daily breakdowns and weekly totals.

Prompt example:

```
Build a weekly timesheet page at /timesheets. Add a week selector (Mon-Sun). Fetch all users in the workspace and time entries for the selected week from my Clockify Edge Function. Render a table: rows are team members, columns are Mon through Sun plus a Total column. Cell value = total hours logged that day. Clicking a cell opens a detail panel with that day's individual time entries. Add a column for each project showing project-level totals per person. Color cells red if under 6 hours on a workday. Export to CSV button. Show a totals row at the bottom and a totals column on the right.
```

### Project profitability tracker with hourly rates

A freelance studio uses Clockify to track project hours and wants a profitability dashboard that multiplies hours by each team member's hourly rate to calculate project revenue vs estimated budget. The Edge Function fetches time entries per project with user breakdowns, and the frontend multiplies by configurable rates stored in Supabase to generate margin calculations.

Prompt example:

```
Build a project profitability dashboard at /profitability. Fetch all active Clockify projects from my Edge Function. For each project, fetch total time entries this month grouped by user. Multiply each user's hours by their hourly rate (stored in a Supabase user_rates table keyed by Clockify user ID). Calculate: total revenue (hours * rate sum), project budget from Supabase projects table, gross margin, and hours remaining before budget exceeded. Display in a sortable table with color-coded margin indicators. Click a project to see the full breakdown by team member.
```

## Troubleshooting

### API returns 403 Forbidden even though the API key looks correct

Cause: The X-Api-Key header is being set incorrectly — either using 'Authorization: Bearer' instead of 'X-Api-Key', or there's a typo in the header name. Clockify uses a custom header, not the standard Authorization header.

Solution: Verify your Edge Function is setting the header as 'X-Api-Key' (with hyphen, exact capitalization). Do not use 'Authorization: Bearer' or 'Api-Key' — Clockify's API specifically requires 'X-Api-Key' as the custom header name. Check Cloud → Logs to see the exact request headers being sent.

```
// Correct authentication header
const authHeaders = {
  "X-Api-Key": apiKey,  // NOT Authorization: Bearer
  "Content-Type": "application/json",
};
```

### Stopping the timer returns 404 or does not stop the running entry

Cause: The stop_timer PATCH endpoint requires the running entry to exist for the specified user. If the userId passed doesn't match the user who started the timer, or there is no running timer, the request returns 404 or 400.

Solution: Always get the current timer's user ID from the get_current_timer response before stopping. The stop_timer action uses PATCH /workspaces/{workspaceId}/user/{userId}/time-entries — the userId must match who owns the running timer. If you're building a multi-user app, store each user's Clockify user ID in Supabase and use that for timer operations.

### Time entry durations display as 'PT0S' or parse to 0 hours

Cause: Clockify returns duration as PT0S for in-progress (running) time entries since the end time is not yet set. These entries should display as elapsed time calculated from the start time, not parsed from the duration field.

Solution: When rendering time entries, check if the timeInterval.end field is null or empty — that indicates an in-progress entry. For in-progress entries, calculate elapsed time as Date.now() - new Date(entry.timeInterval.start).getTime() instead of parsing the duration field.

```
const getDuration = (entry) => {
  if (!entry.timeInterval.end) {
    // Running entry — calculate elapsed
    return (Date.now() - new Date(entry.timeInterval.start).getTime()) / 3600000;
  }
  // Completed entry — parse ISO duration PT2H30M
  const match = entry.duration?.match(/PT(?:(\d+)H)?(?:(\d+)M)?/);
  return ((+match?.[1] || 0) + (+match?.[2] || 0) / 60);
};
```

### Fetching time entries returns only 50 results even for weeks with more entries

Cause: Clockify paginates time entry responses with a default page size of 50 items. Weeks with active teams can have hundreds of entries across all users.

Solution: Add page-size=200 to your list_time_entries query parameters to increase the maximum per page (Clockify supports up to 200). For teams with more than 200 entries per user per period, implement pagination by checking the response count and requesting additional pages with the page parameter.

```
// In the Edge Function, set a higher page size
q.set("page-size", "200");
// For pagination, add:
if (params.page) q.set("page", String(params.page));
```

## Frequently asked questions

### Does Clockify have a native connector in Lovable?

No, Clockify is not one of Lovable's 17 shared connectors. Integration requires an Edge Function proxy using the Clockify REST API v1. The API key is stored in Cloud Secrets and sent as an X-Api-Key header — it is never exposed to browser code.

### Is Clockify's API available on the free plan?

Yes — Clockify's REST API is available on all plans including the completely free tier. This is one of Clockify's key differentiators: there are no per-user costs and no API access fees. Advanced reporting endpoints and some bulk operations require paid tiers, but basic time entry CRUD and workspace management work on the free plan.

### How do I find a Clockify user's ID for making user-scoped requests?

Call the list_users action to get all workspace members — each user object includes an 'id' field (a 24-character hex string). You can also get the current API key owner's user ID via GET /user (without workspace scoping). Store the mapping of your app's users to their Clockify user IDs in Supabase so time entry requests always use the correct user ID.

### Can multiple users in my app track time against the same Clockify project?

Yes — Clockify projects are workspace resources, and multiple users can log time against the same project. Each user's time entries are owned by that user but associated with the shared project. When aggregating project hours, sum time entries across all users filtered by projectId. The list_time_entries action requires a userId, so you'll need to fetch per-user and aggregate client-side or use Clockify's summary reports API for pre-aggregated data.

### How should I handle the case where a user has a timer running in the native Clockify app and then opens my Lovable app?

Call get_current_timer on component mount to check for a running entry. If one exists, display it with the elapsed time calculated from its start field. Your Stop button then stops that same entry. This synchronization ensures your Lovable interface always reflects the true timer state regardless of where it was started. The same approach works in reverse — entries started in your app appear in Clockify's native interface immediately.

---

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