Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Toggl Track

Integrate Toggl Track with Bolt.new by calling Toggl's REST API v9 through Next.js API routes, using basic authentication with your API token. Fetch time entries, project breakdowns, and detailed reports to build a custom time tracking dashboard. Toggl's API uses the simplest auth pattern available — your API token as the username with 'api_token' as the password. Free plan available for up to 5 users.

What you'll learn

  • How to authenticate with Toggl Track's REST API v9 using basic auth with your API token
  • How to fetch time entries, projects, and clients from the Toggl API
  • How to build a time tracking dashboard with project breakdowns and weekly summaries
  • How to use the Toggl Reports API for detailed time reports with date range filtering
  • How to create time entries programmatically from a Bolt.new app
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate19 min read15 minutesOtherApril 2026RapidDev Engineering Team
TL;DR

Integrate Toggl Track with Bolt.new by calling Toggl's REST API v9 through Next.js API routes, using basic authentication with your API token. Fetch time entries, project breakdowns, and detailed reports to build a custom time tracking dashboard. Toggl's API uses the simplest auth pattern available — your API token as the username with 'api_token' as the password. Free plan available for up to 5 users.

Build a Custom Toggl Time Tracking Dashboard in Bolt.new

Toggl Track is the most popular time tracker among freelancers and small agencies, with over 5 million users across 120 countries. Its simplicity — start a timer, add a description, stop it — combined with project and client organization makes it effective for tracking billable hours and understanding where time actually goes. The REST API v9 provides complete access to time entries, projects, clients, workspace members, and detailed reports, making it ideal for building custom billing dashboards, team productivity analytics, or automated invoicing tools.

Toggl's API authentication is refreshingly simple: HTTP basic authentication using your API token as the username and the literal string 'api_token' as the password. No OAuth flows, no token exchange, no expiring tokens — just Base64-encode token:api_token and put it in the Authorization header. This makes building a Bolt.new integration straightforward: create a utility that encodes your credentials, create API routes that proxy requests to Toggl with the Authorization header, and return formatted data to your frontend.

Toggl's API has two separate endpoints: the main Track API at api.track.toggl.com/api/v9/ for managing time entries, projects, and workspace data, and the Reports API at api.track.toggl.com/reports/api/v3/ for aggregated time reports with date range filtering and project/client breakdowns. For dashboard building, you will use both — the main API for current timer status and recent entries, and the Reports API for weekly summaries and project time breakdowns that would be expensive to calculate manually from individual time entry records.

Integration method

Bolt Chat + API Route

Toggl Track's REST API v9 uses HTTP basic authentication with your API token as the username and the string 'api_token' as the password. All API calls are proxied through Next.js API routes to keep the token server-side and avoid CORS. The API provides time entries, projects, clients, and workspace data, plus a separate Reports API for detailed time summaries and breakdowns. All API calls are outbound from Bolt's WebContainer, so they work in the development preview. Toggl does not use webhooks — polling the API for new entries is the standard pattern.

Prerequisites

  • A Toggl Track account — free tier supports up to 5 users at https://toggl.com/track
  • Your Toggl API token — found in Profile Settings → scroll to bottom → API Token (click 'Click to reveal')
  • Your Toggl Workspace ID — found in the URL when viewing your workspace (toggl.com/app/workspaces/{workspaceId}/...)
  • At least a few time entries and projects in Toggl for testing the API responses
  • A Bolt.new account with a Next.js project open

Step-by-step guide

1

Get Your Toggl API Token and Configure Authentication

Toggl uses HTTP basic authentication for its API — the same authentication scheme that powers simple username/password protected web pages, but with your API token as the credential. This is one of the simplest API auth patterns you'll encounter: no OAuth flows, no JWT, no token expiry. Your API token is a permanent credential tied to your Toggl account. To find your API token, log into toggl.com, click your profile picture in the top-right corner, select Profile Settings, and scroll to the bottom of the page. You will see an API Token section with 'Click to reveal'. Click it, copy the token (a 32-character hexadecimal string), and store it in your Bolt.new project's .env file. Toggl's basic auth encoding is: take your API token, append a colon, then append the literal string 'api_token' (this is a Toggl convention — not your actual password). Base64-encode the result. In JavaScript: Buffer.from(`${apiToken}:api_token`).toString('base64'). This encoded string goes in the Authorization header as 'Basic {encoded}'. Every Toggl API request requires this header. You also need your Workspace ID for workspace-level API calls (reports, team data, project management). Find it in your Toggl URL when browsing your workspace — it appears as a number in the URL path. Alternatively, call GET /me/workspaces which returns all workspaces you belong to, each with an id field. For single-workspace Toggl accounts, the workspace ID is in the response of GET /me as well. Store it in your .env as TOGGL_WORKSPACE_ID. Toggl's rate limit is 1 request per second. This is generous for dashboard use but means you should avoid making bulk API calls in tight loops. For the dashboard patterns in this guide, you will make 2-4 API calls on page load — well within rate limits.

.env
1# .env add to project root in Bolt
2# Found at: toggl.com Profile Settings scroll to bottom API Token
3TOGGL_API_TOKEN=your_32_char_api_token_here
4# Found in URL: toggl.com/app/workspaces/{THIS_NUMBER}/...
5TOGGL_WORKSPACE_ID=12345678
6# Optional: hourly rate for billing calculations (cents per hour)
7TOGGL_HOURLY_RATE_CENTS=15000

Pro tip: Toggl API tokens do not expire unless you regenerate them in Profile Settings. Treat your Toggl token with the same care as a password — it grants full access to read and write all your time data. Never use the NEXT_PUBLIC_ prefix; the token must only be used in server-side API routes.

Expected result: Your Toggl API token and Workspace ID are saved in .env. You are ready to create the API client in the next step.

2

Build the Toggl API Client

Create a reusable Toggl API client utility that handles authentication encoding and makes requests to both the Track API and the Reports API. The client encodes your credentials once at initialization and includes them in every request's Authorization header. Toggl has two API bases: https://api.track.toggl.com/api/v9/ for time entries and metadata, and https://api.track.toggl.com/reports/api/v3/ for aggregated reports. Both use the same basic auth credentials. Create a single client that can target either base URL. The key endpoints you will use: GET /me for the authenticated user's profile and workspace list, GET /me/time_entries for recent time entries (supports since and before query params for date filtering), GET /me/time_entries/current for the actively running timer (returns null if no timer is running), GET /me/projects for all projects the user has access to, and GET /me/clients for client data. For reports, POST /workspace/{workspaceId}/summary/time_entries returns aggregated time by project and user for a date range. Time entries in Toggl have a duration field that is positive when the entry is stopped (duration in seconds) and negative when it is still running (a Unix timestamp of when it started, stored as a negative number). To calculate the duration of a running timer: current Unix time (seconds) - Math.abs(entry.duration). Handle this in your client utility so the dashboard displays correctly for both stopped and running timers. Create the client as a class or module-level functions. All functions should be async and throw errors on non-200 responses so your API routes can catch and return appropriate error responses to the frontend. Put all API logic in the client utility and keep the API routes thin — they should just call the client functions and return formatted JSON.

Bolt.new Prompt

Create a Toggl API client at lib/toggl-client.ts. Implement basic auth encoding using Buffer.from(`${token}:api_token`).toString('base64'). Build functions: getMe() for user profile, getTimeEntries(since, before) for time entries with optional date range, getCurrentTimer() for running timer or null, getProjects() for all projects, getSummaryReport(workspaceId, startDate, endDate) for the Reports API summary. Handle the running timer duration (negative duration field). Create an API route app/api/toggl/dashboard/route.ts that calls getMe(), getTimeEntries() for today, and getCurrentTimer() in parallel, returns combined dashboard data. Test by visiting /api/toggl/dashboard in the Bolt preview.

Paste this in Bolt.new chat

lib/toggl-client.ts
1// lib/toggl-client.ts
2const TRACK_BASE = 'https://api.track.toggl.com/api/v9';
3const REPORTS_BASE = 'https://api.track.toggl.com/reports/api/v3';
4
5function getAuthHeader(): string {
6 const token = process.env.TOGGL_API_TOKEN;
7 if (!token) throw new Error('TOGGL_API_TOKEN not configured');
8 return 'Basic ' + Buffer.from(`${token}:api_token`).toString('base64');
9}
10
11async function togglRequest<T>(url: string, options: RequestInit = {}): Promise<T> {
12 const res = await fetch(url, {
13 ...options,
14 headers: {
15 Authorization: getAuthHeader(),
16 'Content-Type': 'application/json',
17 ...options.headers,
18 },
19 });
20
21 if (res.status === 404) return null as T;
22
23 if (!res.ok) {
24 const text = await res.text();
25 throw new Error(`Toggl API error ${res.status}: ${text}`);
26 }
27
28 return res.json();
29}
30
31export interface TogglTimeEntry {
32 id: number;
33 description?: string;
34 project_id?: number;
35 start: string;
36 stop?: string;
37 duration: number; // negative = still running
38 workspace_id: number;
39 tags?: string[];
40}
41
42export interface TogglProject {
43 id: number;
44 name: string;
45 color: string;
46 client_id?: number;
47 active: boolean;
48}
49
50export function getDurationSeconds(entry: TogglTimeEntry): number {
51 if (entry.duration >= 0) return entry.duration;
52 // Running timer: duration is negative Unix start timestamp
53 return Math.floor(Date.now() / 1000) - Math.abs(entry.duration);
54}
55
56export function formatDuration(seconds: number): string {
57 const h = Math.floor(seconds / 3600);
58 const m = Math.floor((seconds % 3600) / 60);
59 return h > 0 ? `${h}h ${m}m` : `${m}m`;
60}
61
62export async function getMe() {
63 return togglRequest<{ id: number; email: string; fullname: string; default_workspace_id: number }>(
64 `${TRACK_BASE}/me`
65 );
66}
67
68export async function getCurrentTimer(): Promise<TogglTimeEntry | null> {
69 return togglRequest<TogglTimeEntry | null>(`${TRACK_BASE}/me/time_entries/current`);
70}
71
72export async function getTimeEntries(since?: string, before?: string): Promise<TogglTimeEntry[]> {
73 const params = new URLSearchParams();
74 if (since) params.set('since', since);
75 if (before) params.set('before', before);
76 const query = params.toString() ? `?${params}` : '';
77 return togglRequest<TogglTimeEntry[]>(`${TRACK_BASE}/me/time_entries${query}`);
78}
79
80export async function getProjects(): Promise<TogglProject[]> {
81 return togglRequest<TogglProject[]>(`${TRACK_BASE}/me/projects`);
82}
83
84export async function getSummaryReport(
85 workspaceId: number,
86 startDate: string,
87 endDate: string
88) {
89 return togglRequest<{ groups: Array<{ id: number; seconds: number; sub_groups: Array<{ id: number; seconds: number }> }> }>(
90 `${REPORTS_BASE}/workspace/${workspaceId}/summary/time_entries`,
91 {
92 method: 'POST',
93 body: JSON.stringify({
94 start_date: startDate,
95 end_date: endDate,
96 grouping: 'projects',
97 sub_grouping: 'users',
98 }),
99 }
100 );
101}

Pro tip: The Toggl Reports API requires a POST request with a JSON body containing start_date and end_date (in YYYY-MM-DD format), even though it is a read operation. This is different from the Track API which uses GET with query params for time entry filtering.

Expected result: The /api/toggl/dashboard endpoint returns the user's profile, today's time entries, and current timer status. The running timer's duration updates correctly when the API is called multiple times.

3

Build the Time Tracking Dashboard

With the Toggl client working, build the main dashboard UI that gives users a better view of their time data than Toggl's built-in reports. The dashboard consists of three sections: today's activity summary, a weekly chart, and a project breakdown. For today's summary: fetch all time entries for today (pass today's date as the since parameter), calculate total seconds tracked, format as hours and minutes, and show which projects were worked on. If there is a running timer, show a pulsing indicator with the current project name and elapsed time. Display the active timer prominently at the top — it is the highest-priority information for someone checking their dashboard mid-day. For the weekly chart: fetch time entries for the past 7 days, group them by day, and sum the durations. Use Recharts BarChart to display hours per day — this gives an immediate visual of which days were most productive. Pass the entries through your formatDuration utility for axis labels. For the project breakdown: use the Toggl Summary Reports API which aggregates entries by project automatically — this is much faster than doing it yourself from raw time entry records. The summary response includes total seconds per project. Join with your projects list (fetched from getProjects()) to get project names and colors. Display as a horizontal bar chart or a table with colored project name pills. For the billing calculation: multiply total project hours by your hourly rate. Store the rate in TOGGL_HOURLY_RATE_CENTS environment variable (in cents to avoid floating point issues). This creates a simple dashboard that shows estimated billable amount for the week without needing Toggl Premium's billing features. All these API calls should be made in parallel using Promise.all — don't await them sequentially. A typical dashboard load fetches profile, time entries for today, and the weekly summary report simultaneously, completing in about 300-500ms total.

Bolt.new Prompt

Build a Toggl time tracking dashboard at app/dashboard/time/page.tsx. Fetch data from /api/toggl/dashboard which should call the Toggl API in parallel for: today's entries, this week's summary report (grouped by project), current running timer, and projects list. Build the page with: (1) A running timer banner at the top (green if active, gray if not) showing description, project name, and elapsed time formatted as h:mm:ss — update every second with useEffect. (2) A 'Today' stat card showing total hours logged. (3) A weekly bar chart using recharts BarChart with hours per day for the past 7 days. (4) A project breakdown table showing project name (with color dot), total hours this week, and billable amount calculated from process.env.NEXT_PUBLIC_HOURLY_RATE. Use shadcn/ui Card components and Tailwind CSS.

Paste this in Bolt.new chat

app/api/toggl/dashboard/route.ts
1// app/api/toggl/dashboard/route.ts
2import { NextResponse } from 'next/server';
3import {
4 getMe,
5 getTimeEntries,
6 getCurrentTimer,
7 getProjects,
8 getSummaryReport,
9 getDurationSeconds,
10} from '@/lib/toggl-client';
11
12export async function GET() {
13 try {
14 const today = new Date();
15 const weekAgo = new Date(today);
16 weekAgo.setDate(today.getDate() - 7);
17
18 const todayStr = today.toISOString().split('T')[0];
19 const weekAgoStr = weekAgo.toISOString().split('T')[0];
20
21 // Fetch all data in parallel
22 const [me, todayEntries, currentTimer, projects, weekSummary] = await Promise.all([
23 getMe(),
24 getTimeEntries(todayStr),
25 getCurrentTimer(),
26 getProjects(),
27 getSummaryReport(
28 parseInt(process.env.TOGGL_WORKSPACE_ID!),
29 weekAgoStr,
30 todayStr
31 ),
32 ]);
33
34 // Calculate today's total
35 const todaySeconds = todayEntries.reduce(
36 (sum, entry) => sum + getDurationSeconds(entry),
37 0
38 );
39
40 // Build project map for name/color lookups
41 const projectMap = Object.fromEntries(
42 projects.map(p => [p.id, { name: p.name, color: p.color }])
43 );
44
45 // Format weekly summary with project names
46 const projectBreakdown = (weekSummary?.groups || []).map(group => ({
47 projectId: group.id,
48 projectName: projectMap[group.id]?.name || 'No project',
49 color: projectMap[group.id]?.color || '#888888',
50 totalSeconds: group.seconds,
51 hoursFormatted: `${Math.floor(group.seconds / 3600)}h ${Math.floor((group.seconds % 3600) / 60)}m`,
52 }));
53
54 return NextResponse.json({
55 user: { name: me.fullname, email: me.email },
56 today: {
57 totalSeconds: todaySeconds,
58 entryCount: todayEntries.length,
59 },
60 currentTimer: currentTimer
61 ? {
62 description: currentTimer.description || 'No description',
63 projectName: currentTimer.project_id ? projectMap[currentTimer.project_id]?.name : null,
64 startedAt: currentTimer.start,
65 elapsedSeconds: getDurationSeconds(currentTimer),
66 }
67 : null,
68 projectBreakdown,
69 });
70 } catch (error) {
71 const message = error instanceof Error ? error.message : 'Failed to load dashboard';
72 return NextResponse.json({ error: message }, { status: 500 });
73 }
74}

Pro tip: For the running timer's elapsed time display, fetch the dashboard data once on load and then calculate elapsed time client-side using setInterval(1000) and the startedAt timestamp. This avoids making an API call every second just to update the timer display.

Expected result: The time tracking dashboard loads and shows today's tracked hours, the running timer status (if active), a weekly bar chart, and a project breakdown table with hours tracked per project this week.

4

Add Time Entry Creation and Deploy

Complete the integration by adding the ability to create time entries from your Bolt.new app — useful when building internal tools that log time to specific Toggl projects automatically. The time entry creation API is at POST /workspaces/{workspaceId}/time_entries with a JSON body containing the description, start time in ISO 8601 format, duration in seconds (-1 for a running timer), workspace_id, and optionally project_id and tags. For a running timer (something you start now and stop later), set duration to -1. Toggl then starts counting the duration from the start time you provide. To stop a running timer, use PUT /workspaces/{workspaceId}/time_entries/{timeEntryId}/stop. In your Bolt.new app, the timer creation UI should have: a description input, a project dropdown populated from your Toggl projects, a start button that calls the create API with the current timestamp, and a stop button that appears when a timer is running. After stopping, show the final duration. Deploy to Netlify or Bolt Cloud after building the core features. Toggl does not use webhooks — there are no incoming connections to configure — so deployment is straightforward. Set TOGGL_API_TOKEN and TOGGL_WORKSPACE_ID as server-side environment variables on Netlify (Site settings → Environment variables). These are not prefixed with NEXT_PUBLIC_ since they are used only in server-side API routes. After deployment, test the full flow: start a timer, verify it appears in Toggl's web app, stop it, and confirm the time entry is saved with the correct duration.

Bolt.new Prompt

Add time entry creation to the dashboard. Create API routes: POST /api/toggl/time-entries to create a new entry (accepts description and project_id, uses current time as start, duration=-1 for running timer), DELETE /api/toggl/time-entries/current to stop the running timer. Build a timer control component showing: a description input, project dropdown from GET /api/toggl/projects, a Start Timer button that creates a time entry, and a Stop Timer button (visible when currentTimer exists) that calls the stop endpoint. After stopping, show a toast notification with the final duration. Prepare the app for Netlify deployment with a netlify.toml and verify all environment variable references use process.env without NEXT_PUBLIC_ prefix.

Paste this in Bolt.new chat

app/api/toggl/time-entries/route.ts
1// app/api/toggl/time-entries/route.ts
2import { NextResponse } from 'next/server';
3
4export async function POST(request: Request) {
5 try {
6 const { description, project_id } = await request.json();
7 const workspaceId = process.env.TOGGL_WORKSPACE_ID;
8 const token = process.env.TOGGL_API_TOKEN;
9
10 const authHeader = 'Basic ' + Buffer.from(`${token}:api_token`).toString('base64');
11
12 const body = {
13 description: description || '',
14 project_id: project_id || null,
15 start: new Date().toISOString(),
16 duration: -1, // -1 means running timer
17 workspace_id: parseInt(workspaceId!),
18 created_with: 'my-bolt-app',
19 };
20
21 const res = await fetch(
22 `https://api.track.toggl.com/api/v9/workspaces/${workspaceId}/time_entries`,
23 {
24 method: 'POST',
25 headers: {
26 Authorization: authHeader,
27 'Content-Type': 'application/json',
28 },
29 body: JSON.stringify(body),
30 }
31 );
32
33 if (!res.ok) {
34 const error = await res.text();
35 return NextResponse.json({ error }, { status: res.status });
36 }
37
38 const entry = await res.json();
39 return NextResponse.json({ entry }, { status: 201 });
40 } catch (error) {
41 const message = error instanceof Error ? error.message : 'Failed to create time entry';
42 return NextResponse.json({ error: message }, { status: 500 });
43 }
44}
45
46// app/api/toggl/time-entries/stop/route.ts
47import { NextResponse } from 'next/server';
48
49export async function POST() {
50 try {
51 const workspaceId = process.env.TOGGL_WORKSPACE_ID;
52 const token = process.env.TOGGL_API_TOKEN;
53 const authHeader = 'Basic ' + Buffer.from(`${token}:api_token`).toString('base64');
54
55 // First get the current timer ID
56 const currentRes = await fetch(
57 'https://api.track.toggl.com/api/v9/me/time_entries/current',
58 { headers: { Authorization: authHeader } }
59 );
60 const current = await currentRes.json();
61
62 if (!current?.id) {
63 return NextResponse.json({ error: 'No running timer' }, { status: 404 });
64 }
65
66 // Stop the timer
67 const stopRes = await fetch(
68 `https://api.track.toggl.com/api/v9/workspaces/${workspaceId}/time_entries/${current.id}/stop`,
69 { method: 'PATCH', headers: { Authorization: authHeader } }
70 );
71
72 const stopped = await stopRes.json();
73 return NextResponse.json({ entry: stopped });
74 } catch (error) {
75 const message = error instanceof Error ? error.message : 'Failed to stop timer';
76 return NextResponse.json({ error: message }, { status: 500 });
77 }
78}

Pro tip: Include created_with in the time entry creation body with your app name — this helps users identify which entries were created by your integration when they review their time in Toggl's web interface.

Expected result: Starting a timer from your Bolt.new app creates a time entry visible in Toggl's web interface. Stopping it saves the entry with the correct duration. After deploying to Netlify, all features work identically with the environment variables configured in Netlify's dashboard.

Common use cases

Freelancer Time and Billing Dashboard

Build a personal dashboard that shows billable hours per client this week and month, total hours tracked today, and a running timer status. Ideal for freelancers who use Toggl for time tracking and want a custom view beyond Toggl's built-in reports, with project rate calculations and estimated invoice amounts.

Bolt.new Prompt

Build a freelancer time tracking dashboard in Next.js using the Toggl API. Create API routes to fetch: today's time entries from GET /me/time_entries, the summary report for this week from the Toggl Reports API with project and client breakdown, and the current running timer from GET /me/time_entries/current. Build a dashboard page with: a 'Today' card showing hours tracked today, a 'This Week' chart using recharts showing hours per day, a project breakdown table with hours and estimated billing (hourly rate from env var), and a timer status indicator showing if a timer is currently running. Use process.env.TOGGL_API_TOKEN for auth.

Copy this prompt to try it in Bolt.new

Team Time Report by Project

Build a team time reporting dashboard for managers showing how many hours each team member has logged per project over a date range. Uses Toggl's workspace-level API to access all workspace members' time data and the Reports API for aggregated summaries with user breakdowns.

Bolt.new Prompt

Create a team time report page in Next.js for a Toggl workspace. Create an API route at app/api/toggl/reports/team/route.ts that calls the Toggl Reports API v3 detailed search endpoint (POST /workspace/{workspaceId}/search/time_entries) with date range from query params. Return hours grouped by user and project. Build a report page at /reports/team with a date range picker using react-day-picker, a table showing rows for each team member with columns for each project and total hours, export to CSV functionality, and a bar chart comparing total hours per person this week. Use process.env.TOGGL_API_TOKEN and process.env.TOGGL_WORKSPACE_ID.

Copy this prompt to try it in Bolt.new

Automated Time Entry Tracker for Billable Work

Build an internal tool that creates Toggl time entries automatically when specific actions happen in your Bolt.new app — for example, when a support ticket is opened, when a client meeting is logged, or when a task is marked complete. Combines Toggl's time entry creation API with other data sources in your app.

Bolt.new Prompt

Create a time entry creation integration with Toggl in Next.js. Build an API route at app/api/toggl/time-entries/route.ts that accepts POST requests with description, project_id, start (ISO timestamp), and duration_seconds. Use the Toggl API to create the time entry at POST /me/time_entries or POST /workspaces/{wid}/time_entries. Also create a GET handler that fetches recent time entries. Build a simple form at /time/log with a description input, project dropdown (populated from GET /me/projects), and duration picker. After submission, show the created entry with its Toggl ID and a link to view it in Toggl. Use process.env.TOGGL_API_TOKEN.

Copy this prompt to try it in Bolt.new

Troubleshooting

All Toggl API requests return 403 Forbidden

Cause: The basic auth encoding is incorrect, the API token has been regenerated since it was saved, or the TOGGL_API_TOKEN environment variable is not being read by the server.

Solution: Verify the token value in your .env file matches what is shown in Toggl → Profile Settings → API Token. Ensure the encoding uses exactly 'api_token' as the password (not your account password, not empty). Restart the Bolt development server after changing .env values.

typescript
1// Verify the exact encoding pattern:
2const token = process.env.TOGGL_API_TOKEN;
3const encoded = Buffer.from(`${token}:api_token`).toString('base64');
4// The 'api_token' string is ALWAYS the password for Toggl basic auth
5// NOT your account password, NOT empty string

Direct Toggl API calls from React components return CORS errors

Cause: Toggl's API does not allow cross-origin requests from browser clients. Direct fetch calls from React components to api.track.toggl.com are blocked by the browser's CORS policy.

Solution: Move all Toggl API calls to Next.js API routes (server-side). Your React components should call /api/toggl/* routes (same-origin), which then proxy requests to Toggl's servers server-side without CORS restrictions.

typescript
1// WRONG: calling Toggl directly from React
2const res = await fetch('https://api.track.toggl.com/api/v9/me/time_entries');
3
4// CORRECT: call your own API route
5const res = await fetch('/api/toggl/time-entries');

Running timer shows incorrect elapsed time or always shows 0 seconds

Cause: The running timer's duration field is a negative Unix timestamp, not a duration. Client code is using Math.abs() of the duration value as seconds rather than calculating the difference from now.

Solution: For a running timer (duration < 0), calculate elapsed seconds as: Math.floor(Date.now() / 1000) - Math.abs(entry.duration). The negative duration field stores the Unix timestamp when the timer started as a negative number.

typescript
1function getDurationSeconds(entry: TogglTimeEntry): number {
2 if (entry.duration >= 0) {
3 return entry.duration; // Stopped timer
4 }
5 // Running timer: duration is negative Unix start timestamp
6 return Math.floor(Date.now() / 1000) - Math.abs(entry.duration);
7}

Toggl Reports API returns empty groups array despite having time entries in that date range

Cause: The Reports API is being called with GET instead of POST, or the start_date and end_date are in the wrong format (should be YYYY-MM-DD, not ISO timestamp).

Solution: The Toggl Reports API summary endpoint requires a POST request with a JSON body, even though it is reading data. Ensure your request uses method: 'POST' and the body contains start_date and end_date as YYYY-MM-DD strings (not full ISO timestamps with time components).

typescript
1// CORRECT: POST with date-only strings
2const res = await fetch(`${REPORTS_BASE}/workspace/${workspaceId}/summary/time_entries`, {
3 method: 'POST', // Must be POST
4 headers: { Authorization: authHeader, 'Content-Type': 'application/json' },
5 body: JSON.stringify({
6 start_date: '2025-04-01', // YYYY-MM-DD only, no time component
7 end_date: '2025-04-22',
8 grouping: 'projects',
9 }),
10});

Best practices

  • Never use NEXT_PUBLIC_ prefix for TOGGL_API_TOKEN — it must only be used in server-side API routes to prevent exposing your time tracking credentials in the client bundle
  • Calculate running timer elapsed time client-side with setInterval rather than polling the API every second — this avoids hitting Toggl's 1 request-per-second rate limit while keeping the timer display accurate
  • Use the Toggl Reports API for weekly and monthly aggregates rather than fetching all time entries and summing them yourself — the Reports API handles large date ranges efficiently server-side
  • Cache project and client lists for 5-10 minutes since they change infrequently — you don't need to fetch the project list on every time entry API call
  • Include a created_with field in all time entries created by your app to make entries identifiable in Toggl's interface
  • Handle the case where a project_id is null — Toggl allows time entries without projects, so always check before doing a project name lookup
  • Deploy to Netlify or Bolt Cloud before testing features in a team context — Toggl's workspace API provides access to all team member data which is best tested in a real server environment

Alternatives

Frequently asked questions

How does Toggl's basic auth work and is it secure?

Toggl uses HTTP basic authentication with your API token as the username and the literal string 'api_token' as the password. This is Base64-encoded (not encrypted) and sent in the Authorization header. It is secure over HTTPS because TLS encrypts the header contents in transit. Never use basic auth over plain HTTP, and always handle the token server-side in Next.js API routes — never in client-side code where it is visible in browser DevTools.

Can I use the Toggl API with a free Toggl Track account?

Yes. Toggl's API is available on the free plan, which supports up to 5 workspace users. The Reports API is also available on the free plan for fetching time summaries. Some features like billable rates, required fields, and team-level management require paid plans (Starter at $9/user/month, Premium at $18/user/month), but basic time entry access and reports work without payment.

How do I handle the running timer duration field?

When a timer is actively running, Toggl sets the duration field to a negative number equal to the Unix timestamp (in seconds) when the timer started, multiplied by -1. To get the current elapsed duration, calculate: Math.floor(Date.now() / 1000) - Math.abs(entry.duration). A positive duration means the entry is stopped and the value is the total seconds. Always check if duration >= 0 before treating it as an elapsed time.

Does Toggl support webhooks for real-time updates?

Toggl does not support webhooks for individual user accounts via the public API. There are no event notifications when a timer starts, stops, or a time entry is created. For real-time timer status in your dashboard, poll the /me/time_entries/current endpoint every 30-60 seconds, or calculate elapsed time client-side from the last known start time and update the display with setInterval without making API calls.

What is the difference between the Track API and the Reports API?

The Track API (api.track.toggl.com/api/v9) provides CRUD operations on individual time entries, projects, clients, and workspace data. The Reports API (api.track.toggl.com/reports/api/v3) provides pre-aggregated time summaries by project, user, client, or tag — use it when you need totals over a date range without fetching and summing every individual entry yourself. Both use the same basic auth credentials. The Reports API uses POST requests with a JSON body even for read operations.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.