Skip to main content
RapidDev - Software Development Agency
replit-integrationsStandard API Integration

How to Integrate Replit with Toggl

To integrate Replit with Toggl Track, generate a Toggl API token from your profile settings, store it in Replit Secrets (lock icon πŸ”’), and call the Toggl Track API v9 from Python or Node.js server-side code to create time entries, fetch project data, generate detailed reports, and build custom time tracking workflows. Use Autoscale for web apps or Reserved VM for scheduled reporting jobs.

What you'll learn

  • How to generate a Toggl API token and retrieve your Workspace ID
  • How to store Toggl credentials securely in Replit Secrets
  • How to create, start, stop, and query time entries using Python and Node.js
  • How to fetch detailed and summary reports from the Toggl Reports API
  • How to build automated time tracking and billing workflows using Toggl from Replit
Book a free consultation
4.9Clutch rating ⭐
600+Happy partners
17+Countries served
190+Team members
Intermediate18 min read25 minutesProductivityMarch 2026RapidDev Engineering Team
TL;DR

To integrate Replit with Toggl Track, generate a Toggl API token from your profile settings, store it in Replit Secrets (lock icon πŸ”’), and call the Toggl Track API v9 from Python or Node.js server-side code to create time entries, fetch project data, generate detailed reports, and build custom time tracking workflows. Use Autoscale for web apps or Reserved VM for scheduled reporting jobs.

Why Connect Replit to Toggl Track?

Toggl Track is one of the most widely used time tracking tools among freelancers, agencies, and remote teams β€” with over 5 million users and an API that covers the full spectrum of time tracking workflows. Connecting your Replit app to Toggl enables you to automate time entry creation from external project management tools, build custom reporting dashboards that pull billable hours for client invoicing, sync time data with accounting software, and create internal tools that eliminate the need for your team to manually switch to the Toggl interface for every time entry.

The Toggl ecosystem consists of two separate APIs: the main Toggl Track API (v9) for creating and managing time entries, projects, clients, and workspaces, and the Toggl Reports API (v3) for pre-aggregated analytics across different time periods. Both use the same API token authentication. The Reports API is particularly powerful for generating client billing summaries and team productivity reports without building your own aggregation logic.

Replit's Secrets system (lock icon πŸ”’ in the sidebar) keeps your Toggl API token secure. The token has the same access level as your Toggl account β€” it can read and write all workspace data. Store it as TOGGL_API_TOKEN in Replit Secrets and access it with os.environ['TOGGL_API_TOKEN'] in Python or process.env.TOGGL_API_TOKEN in Node.js. The Toggl API uses unusual Basic Auth where the token is the username and the literal string 'api_token' is the password β€” this is a Toggl-specific convention.

Integration method

Standard API Integration

You connect Replit to Toggl Track by generating an API token from your profile settings, storing it in Replit Secrets, and calling the Toggl Track API v9 using HTTP Basic Auth with the token as the username and 'api_token' as the password. A separate Reports API at reports.toggl.com provides pre-aggregated summary and detailed reports. Both APIs require your Workspace ID, which you retrieve from the /me/workspaces endpoint on first setup.

Prerequisites

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

Step-by-step guide

1

Get Your Toggl API Token and Workspace ID

Log in to your Toggl Track account at track.toggl.com. Click your avatar or name in the bottom-left corner of the sidebar and select 'Profile settings'. Scroll to the very bottom of the Profile Settings page to find the 'API Token' section. Your API token is displayed there β€” it is a 32-character hexadecimal string. Click 'Click to reveal' if it is hidden, then copy it. To find your Workspace ID, click on the workspace name in the Toggl sidebar (usually 'My Workspace' or your company name). The Workspace ID appears in the URL as a number: track.toggl.com/1234567/. Alternatively, call the Toggl API with a test request and read the ID from the response. Toggl uses a specific Basic Auth convention: the username is your API token, and the password is the literal string 'api_token'. This is different from most APIs. Both the main API (api.track.toggl.com/api/v9/) and the Reports API (reports.track.toggl.com/api/v3/) use this same authentication pattern. If you work across multiple workspaces (for example, your personal workspace and a client workspace), note both workspace IDs β€” you will specify which workspace to use in each API request.

Pro tip: Your Toggl API token does not expire. However, if you suspect it has been compromised, you can reset it from Profile Settings > API Token > Reset. After resetting, update TOGGL_API_TOKEN in Replit Secrets immediately.

Expected result: You have your 32-character Toggl API token and your numeric Workspace ID copied and ready to store in Replit Secrets.

2

Store Toggl 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: - Key: TOGGL_API_TOKEN β€” Value: your 32-character API token - Key: TOGGL_WORKSPACE_ID β€” Value: your numeric Workspace ID Click 'Add Secret' after each one. Replit encrypts these values and injects them as environment variables at runtime. In Python, access them with os.environ['TOGGL_API_TOKEN']. In Node.js, use process.env.TOGGL_API_TOKEN. The Toggl API uses Basic Auth where the username is the API token and the password is literally 'api_token'. In Python with the requests library, pass auth=(os.environ['TOGGL_API_TOKEN'], 'api_token'). In Node.js with axios, set auth: { username: process.env.TOGGL_API_TOKEN, password: 'api_token' }. Do not confuse this with APIs where the password field is the API secret β€” for Toggl, the string 'api_token' is always the password.

Pro tip: If you work with multiple Toggl workspaces, store each workspace ID with a descriptive name: TOGGL_PERSONAL_WORKSPACE_ID, TOGGL_CLIENT_WORKSPACE_ID. Never hardcode workspace IDs directly in code β€” change them via Secrets when switching environments.

Expected result: Two Secrets (TOGGL_API_TOKEN and TOGGL_WORKSPACE_ID) appear in the Replit Secrets pane with masked values.

3

Manage Time Entries with Python

The Toggl Track API v9 is a REST API at https://api.track.toggl.com/api/v9/. Authentication uses HTTP Basic Auth with your API token as the username and 'api_token' as the password (literally β€” this is Toggl's unconventional but consistent auth pattern). Time entries in Toggl require a description (can be empty), a workspace ID, and a start time in ISO 8601 format with timezone offset. For completed entries, also provide a duration in seconds (positive number). For in-progress timers, use a duration of -1 * (Unix timestamp of start time) β€” Toggl calculates the running duration on the fly. The code below provides a complete Python client including: listing projects and workspaces, starting and stopping timers, creating completed time entries, and fetching recent entries. All timestamps must include timezone information β€” Toggl rejects naive datetimes.

toggl_client.py
1import os
2import requests
3from datetime import datetime, timezone, timedelta
4
5API_TOKEN = os.environ["TOGGL_API_TOKEN"]
6WORKSPACE_ID = int(os.environ["TOGGL_WORKSPACE_ID"])
7
8BASE_URL = "https://api.track.toggl.com/api/v9"
9
10# Toggl uses Basic Auth: API token as username, 'api_token' as password (literal string)
11AUTH = (API_TOKEN, "api_token")
12HEADERS = {"Content-Type": "application/json"}
13
14def get_me() -> dict:
15 """Get current user info and default workspace ID."""
16 response = requests.get(f"{BASE_URL}/me", auth=AUTH)
17 response.raise_for_status()
18 return response.json()
19
20def get_workspaces() -> list:
21 """List all workspaces."""
22 response = requests.get(f"{BASE_URL}/workspaces", auth=AUTH)
23 response.raise_for_status()
24 return response.json()
25
26def get_projects() -> list:
27 """List all projects in the workspace."""
28 url = f"{BASE_URL}/workspaces/{WORKSPACE_ID}/projects"
29 response = requests.get(url, auth=AUTH, params={"active": "true"})
30 response.raise_for_status()
31 return response.json() or []
32
33def get_clients() -> list:
34 """List all clients in the workspace."""
35 url = f"{BASE_URL}/workspaces/{WORKSPACE_ID}/clients"
36 response = requests.get(url, auth=AUTH)
37 response.raise_for_status()
38 return response.json() or []
39
40def create_time_entry(
41 description: str,
42 start: datetime,
43 end: datetime,
44 project_id: int = None,
45 billable: bool = False,
46 tags: list = None
47) -> dict:
48 """
49 Create a completed time entry.
50 duration is calculated automatically from start and end.
51 start and end must be timezone-aware datetime objects.
52 """
53 duration_seconds = int((end - start).total_seconds())
54 data = {
55 "description": description,
56 "start": start.isoformat(),
57 "duration": duration_seconds,
58 "workspace_id": WORKSPACE_ID,
59 "created_with": "replit-integration",
60 "billable": billable
61 }
62 if project_id:
63 data["project_id"] = project_id
64 if tags:
65 data["tags"] = tags
66
67 url = f"{BASE_URL}/workspaces/{WORKSPACE_ID}/time_entries"
68 response = requests.post(url, json=data, auth=AUTH, headers=HEADERS)
69 response.raise_for_status()
70 return response.json()
71
72def start_timer(description: str, project_id: int = None) -> dict:
73 """
74 Start a running timer.
75 Toggl represents running timers with duration = -(Unix timestamp of start).
76 """
77 start = datetime.now(timezone.utc)
78 start_unix = int(start.timestamp())
79 data = {
80 "description": description,
81 "start": start.isoformat(),
82 "duration": -start_unix, # Negative Unix timestamp = running timer
83 "workspace_id": WORKSPACE_ID,
84 "created_with": "replit-integration"
85 }
86 if project_id:
87 data["project_id"] = project_id
88
89 url = f"{BASE_URL}/workspaces/{WORKSPACE_ID}/time_entries"
90 response = requests.post(url, json=data, auth=AUTH, headers=HEADERS)
91 response.raise_for_status()
92 return response.json()
93
94def stop_timer(entry_id: int) -> dict:
95 """Stop a running timer by patching its end time."""
96 stop_time = datetime.now(timezone.utc)
97 url = f"{BASE_URL}/workspaces/{WORKSPACE_ID}/time_entries/{entry_id}/stop"
98 response = requests.patch(url, auth=AUTH, headers=HEADERS)
99 response.raise_for_status()
100 return response.json()
101
102def get_current_timer() -> dict:
103 """Get the currently running timer (if any)."""
104 response = requests.get(f"{BASE_URL}/me/time_entries/current", auth=AUTH)
105 if response.status_code == 200 and response.text != 'null':
106 return response.json()
107 return None
108
109def get_recent_entries(days: int = 7) -> list:
110 """Fetch time entries from the past N days."""
111 end = datetime.now(timezone.utc)
112 start = end - timedelta(days=days)
113 params = {
114 "start_date": start.isoformat(),
115 "end_date": end.isoformat()
116 }
117 response = requests.get(f"{BASE_URL}/me/time_entries", auth=AUTH, params=params)
118 response.raise_for_status()
119 return response.json() or []
120
121# Example usage
122if __name__ == "__main__":
123 me = get_me()
124 print(f"Connected as: {me['fullname']} ({me['email']})")
125
126 projects = get_projects()
127 print(f"Projects: {[p['name'] for p in projects[:5]]}")
128
129 # Log a 1-hour completed entry
130 end_time = datetime.now(timezone.utc)
131 start_time = end_time - timedelta(hours=1)
132 entry = create_time_entry(
133 description="Integration setup",
134 start=start_time,
135 end=end_time,
136 billable=True
137 )
138 print(f"Time entry created: {entry['id']} β€” {entry['description']}")

Pro tip: Toggl's running timer representation is counterintuitive: duration = -(Unix timestamp of start time). This is a legacy design. When displaying a running timer's elapsed time, calculate it as: elapsed_seconds = time.time() + entry['duration'] (adding the negative duration to current time).

Expected result: Running the script prints your Toggl username, lists active projects, and creates a 1-hour time entry visible in your Toggl Track account.

4

Generate Reports with Python and Node.js

The Toggl Reports API v3 (at reports.track.toggl.com/api/v3/) provides pre-aggregated time reports that are far more efficient than fetching raw time entries and aggregating manually. It uses the same API token Basic Auth as the main API. The Reports API offers three report types: Summary (totals by group β€” project, user, client, tag), Detailed (all individual time entries with filtering), and Weekly (hours per day of week). For client billing, the Detailed report filtered by client_ids is most useful. For team analytics, the Summary report grouped by USER is the quickest way to see per-person hours. The Node.js server below provides a billing report endpoint that combines data from the main API (to get client and project names) with the Reports API (to get aggregated hours).

server.js
1const express = require('express');
2const axios = require('axios');
3
4const app = express();
5app.use(express.json());
6
7const API_TOKEN = process.env.TOGGL_API_TOKEN;
8const WORKSPACE_ID = process.env.TOGGL_WORKSPACE_ID;
9
10// Both APIs use the same Basic Auth
11const AUTH = { username: API_TOKEN, password: 'api_token' };
12
13const togglApi = axios.create({
14 baseURL: 'https://api.track.toggl.com/api/v9',
15 auth: AUTH
16});
17
18const reportsApi = axios.create({
19 baseURL: 'https://reports.track.toggl.com/api/v3',
20 auth: AUTH
21});
22
23// Get all active projects
24app.get('/projects', async (req, res) => {
25 try {
26 const response = await togglApi.get(`/workspaces/${WORKSPACE_ID}/projects`, {
27 params: { active: 'true' }
28 });
29 res.json(response.data || []);
30 } catch (err) {
31 res.status(err.response?.status || 500).json({ error: err.message });
32 }
33});
34
35// Get summary report: hours per project for a date range
36// Query params: startDate (YYYY-MM-DD), endDate (YYYY-MM-DD)
37app.get('/reports/summary', async (req, res) => {
38 const { startDate, endDate } = req.query;
39 if (!startDate || !endDate) {
40 return res.status(400).json({ error: 'startDate and endDate are required (YYYY-MM-DD)' });
41 }
42 try {
43 const response = await reportsApi.post(
44 `/workspace/${WORKSPACE_ID}/summary/time_entries`,
45 {
46 start_date: startDate,
47 end_date: endDate,
48 grouping: 'projects',
49 sub_grouping: 'users'
50 }
51 );
52 // Format: map project groups to readable names
53 const summary = (response.data.groups || []).map(group => ({
54 project: group.title || 'No Project',
55 totalSeconds: group.seconds || 0,
56 totalHours: ((group.seconds || 0) / 3600).toFixed(2),
57 users: (group.sub_groups || []).map(sg => ({
58 user: sg.title,
59 hours: (sg.seconds / 3600).toFixed(2)
60 }))
61 }));
62 res.json(summary);
63 } catch (err) {
64 console.error('Reports error:', err.response?.data);
65 res.status(err.response?.status || 500).json({ error: err.message });
66 }
67});
68
69// Get detailed time entries report
70app.get('/reports/detailed', async (req, res) => {
71 const { startDate, endDate, projectId } = req.query;
72 if (!startDate || !endDate) {
73 return res.status(400).json({ error: 'startDate and endDate are required' });
74 }
75 try {
76 const body = {
77 start_date: startDate,
78 end_date: endDate,
79 order_by: 'date',
80 order_dir: 'DESC'
81 };
82 if (projectId) body.project_ids = [parseInt(projectId)];
83
84 const response = await reportsApi.post(
85 `/workspace/${WORKSPACE_ID}/search/time_entries`,
86 body
87 );
88 res.json(response.data || []);
89 } catch (err) {
90 res.status(err.response?.status || 500).json({ error: err.message });
91 }
92});
93
94// Create a time entry
95app.post('/time-entries', async (req, res) => {
96 const { description, start, end, projectId, billable } = req.body;
97 if (!description || !start || !end) {
98 return res.status(400).json({ error: 'description, start, and end are required' });
99 }
100 const startDate = new Date(start);
101 const endDate = new Date(end);
102 const durationSeconds = Math.round((endDate - startDate) / 1000);
103
104 try {
105 const response = await togglApi.post(`/workspaces/${WORKSPACE_ID}/time_entries`, {
106 description,
107 start: startDate.toISOString(),
108 duration: durationSeconds,
109 workspace_id: parseInt(WORKSPACE_ID),
110 project_id: projectId ? parseInt(projectId) : undefined,
111 billable: billable !== false,
112 created_with: 'replit-integration'
113 });
114 res.status(201).json(response.data);
115 } catch (err) {
116 console.error('Create entry error:', err.response?.data);
117 res.status(err.response?.status || 500).json({ error: err.message });
118 }
119});
120
121app.listen(3000, '0.0.0.0', () => {
122 console.log('Toggl Track integration server running on port 3000');
123});

Pro tip: The Toggl Reports API v3 uses POST requests for search/filter operations (not GET), even for read-only data. The request body contains filter parameters like start_date, end_date, project_ids, and user_ids. This is counterintuitive but consistent across all report endpoints.

Expected result: The server starts on port 3000. A GET request to /reports/summary with date parameters returns aggregated hours per project for the specified period.

5

Schedule Automated Reports and Deploy

For automated weekly or monthly reports, use Python's schedule library or Node's node-cron package to run report generation on a recurring schedule from a Replit Reserved VM deployment. The code below is a complete scheduled report script that runs every Monday at 8am, fetches the previous week's time data, and formats a report. Pair this with a notification service (SendGrid for email, Slack webhook for chat) to deliver the report automatically. For deployment, choose Reserved VM in Replit for scheduled jobs β€” it keeps your script running continuously with no cold-start delay. Choose Autoscale for web apps that respond to user requests or webhook triggers. Click 'Deploy' in the Replit toolbar, select your deployment type, and configure the run command to start your server or scheduler script.

weekly_scheduler.py
1import os
2import time
3import schedule
4import requests
5from datetime import datetime, timezone, timedelta
6
7API_TOKEN = os.environ["TOGGL_API_TOKEN"]
8WORKSPACE_ID = int(os.environ["TOGGL_WORKSPACE_ID"])
9AUTH = (API_TOKEN, "api_token")
10REPORTS_URL = f"https://reports.track.toggl.com/api/v3/workspace/{WORKSPACE_ID}"
11
12def seconds_to_hm(seconds: int) -> str:
13 """Format seconds as H:MM."""
14 h = seconds // 3600
15 m = (seconds % 3600) // 60
16 return f"{h}h {m:02d}m"
17
18def generate_weekly_report():
19 """Generate a weekly summary report and print it."""
20 end_date = datetime.now(timezone.utc)
21 start_date = end_date - timedelta(days=7)
22
23 payload = {
24 "start_date": start_date.strftime("%Y-%m-%d"),
25 "end_date": end_date.strftime("%Y-%m-%d"),
26 "grouping": "projects",
27 "sub_grouping": "users"
28 }
29
30 response = requests.post(
31 f"{REPORTS_URL}/summary/time_entries",
32 json=payload,
33 auth=AUTH
34 )
35 response.raise_for_status()
36 data = response.json()
37
38 total_seconds = data.get('seconds', 0)
39 print(f"\n=== Weekly Report ({start_date.date()} to {end_date.date()}) ===")
40 print(f"Total time: {seconds_to_hm(total_seconds)}\n")
41
42 for group in data.get('groups', []):
43 project_name = group.get('title', 'No Project')
44 project_seconds = group.get('seconds', 0)
45 print(f" {project_name}: {seconds_to_hm(project_seconds)}")
46 for sub in group.get('sub_groups', []):
47 print(f" - {sub.get('title', 'Unknown')}: {seconds_to_hm(sub.get('seconds', 0))}")
48
49 print("\n" + "="*50)
50 # In production, send this to Slack or email:
51 # send_slack_message(format_report_for_slack(data))
52 # send_email_report(format_report_html(data))
53
54# Schedule to run every Monday at 8am UTC
55schedule.every().monday.at("08:00").do(generate_weekly_report)
56
57print("Toggl weekly report scheduler started.")
58print("Next run:", schedule.next_run())
59
60# Run immediately on startup for testing
61generate_weekly_report()
62
63while True:
64 schedule.run_pending()
65 time.sleep(60) # Check every minute

Pro tip: Install the schedule library in Replit by running pip install schedule in the Shell, or add it to requirements.txt. For production schedulers, use Reserved VM deployment so the script runs continuously β€” Autoscale will shut down idle instances and miss scheduled runs.

Expected result: Running the scheduler prints the weekly report immediately, then schedules subsequent runs for Monday mornings. With Reserved VM deployment, reports generate automatically each week.

Common use cases

Automated Time Logging from Project Management Tools

When a developer closes a GitHub issue or a team member marks a task complete in Asana or Jira, a Replit webhook automatically creates a Toggl time entry for the time spent on that task. The integration reads start and end timestamps from the task history and posts a corresponding Toggl entry against the linked client project, eliminating manual time logging entirely.

Replit Prompt

Build a Flask webhook endpoint that receives a task completion event with start_time, end_time, task_name, and project_id, looks up the corresponding Toggl project ID from a mapping table, and creates a time entry using TOGGL_API_TOKEN and TOGGL_WORKSPACE_ID from Replit Secrets.

Copy this prompt to try it in Replit

Client Invoice Report Generator

Build a Replit web app that pulls all billable time entries for a specific client from the Toggl Reports API, calculates the invoice amount based on hourly rates stored in your database, and generates a formatted invoice PDF or spreadsheet. Account managers can trigger a report for any client and billing period from a simple web interface without needing access to Toggl.

Replit Prompt

Create an Express server with a POST /invoice endpoint that accepts client_id, start_date, end_date, and hourly_rate, fetches all billable time entries for that client from the Toggl Reports API, and returns a JSON invoice summary showing hours by project and total amount due.

Copy this prompt to try it in Replit

Weekly Productivity Summary Digest

Create a scheduled Replit job that runs every Monday morning, fetches the previous week's time data from Toggl for all team members, and sends a formatted summary to a Slack channel or email. The summary shows total hours per project, billable vs non-billable split, and highlights any team members who logged significantly fewer hours than average.

Replit Prompt

Write a Python script that fetches the Toggl detailed report for the past 7 days using the Reports API, groups entries by user and project, calculates total and billable hours per person, and formats a weekly summary with utilization percentage for each team member.

Copy this prompt to try it in Replit

Troubleshooting

All API requests return 403 Forbidden

Cause: The Toggl API token is incorrect, or the Basic Auth is configured wrong. Toggl requires the API token as the username and the literal string 'api_token' as the password β€” not the token as a bearer token or API key header.

Solution: Verify your API token from Toggl Profile Settings > API Token. Check that your Basic Auth uses the token as the username and exactly 'api_token' (lowercase, no spaces) as the password. Do not use Authorization: Bearer β€” Toggl requires HTTP Basic Auth for this unusual authentication pattern.

typescript
1# Python: correct Toggl authentication
2AUTH = (os.environ['TOGGL_API_TOKEN'], 'api_token') # Token as user, 'api_token' as password
3
4# Node.js equivalent with axios:
5# auth: { username: process.env.TOGGL_API_TOKEN, password: 'api_token' }

POST time entry returns 400 β€” 'Required field missing' or validation error

Cause: The request body is missing required fields or uses incorrect data types. Toggl's time entry endpoint requires workspace_id as an integer (not string), and duration as a positive integer for completed entries.

Solution: Ensure workspace_id is an integer (not a string from os.environ which returns strings). Cast with int(os.environ['TOGGL_WORKSPACE_ID']). Duration must be a positive integer in seconds for completed entries. The start timestamp must be ISO 8601 with timezone offset.

typescript
1# Python: ensure correct data types
2WORKSPACE_ID = int(os.environ['TOGGL_WORKSPACE_ID']) # Must be integer
3
4data = {
5 'workspace_id': WORKSPACE_ID, # Integer, not string
6 'duration': int(duration_seconds), # Integer seconds
7 'start': start.isoformat(), # String with timezone
8 'description': description # String (can be empty but must be present)
9}

Reports API returns 404 β€” endpoint not found

Cause: The Reports API uses a different base URL (reports.track.toggl.com) than the main Toggl API (api.track.toggl.com). Using the main API base URL for report endpoints causes 404 errors.

Solution: Use the correct Reports API base URL: https://reports.track.toggl.com/api/v3/. The workspace ID in the path should be numeric. Report endpoints also use POST (not GET) for filter operations, which is different from typical REST conventions.

typescript
1# Python: use correct Reports API URL and method
2REPORTS_URL = f"https://reports.track.toggl.com/api/v3/workspace/{WORKSPACE_ID}"
3# Reports use POST even for read operations:
4response = requests.post(
5 f"{REPORTS_URL}/summary/time_entries",
6 json={"start_date": start_date, "end_date": end_date},
7 auth=AUTH
8)

get_current_timer returns null even when a timer is running in the Toggl app

Cause: The GET /me/time_entries/current endpoint returns the literal string 'null' (not a JSON null) when no timer is running. If you call response.json() on this response, it returns Python's None. Also, the timer must be running on the same account as the API token.

Solution: Check if response.text == 'null' before calling response.json(). The timer must be associated with the account whose API token you are using. Timers started by other team members in a shared workspace will not appear in your /me/time_entries/current endpoint.

typescript
1def get_current_timer() -> dict | None:
2 response = requests.get(f"{BASE_URL}/me/time_entries/current", auth=AUTH)
3 if response.status_code == 200 and response.text.strip() != 'null':
4 return response.json()
5 return None # No timer running

Best practices

  • Always store TOGGL_API_TOKEN and TOGGL_WORKSPACE_ID in Replit Secrets (lock icon πŸ”’) β€” never in source code or Git history.
  • Cast TOGGL_WORKSPACE_ID to an integer using int() in Python β€” os.environ returns strings, but Toggl's API requires workspace_id as an integer in the request body.
  • Remember that Toggl's Basic Auth uses the API token as the username and the literal string 'api_token' as the password β€” this is Toggl-specific and different from all other API auth patterns.
  • Use the Reports API (reports.track.toggl.com) for aggregated data like billable totals and project summaries β€” it is significantly more efficient than fetching and aggregating raw time entries.
  • Note that the Reports API v3 uses POST requests for all report queries β€” not GET β€” with filter parameters in the request body.
  • Always include timezone information in datetime objects β€” Toggl rejects naive datetimes without timezone offsets.
  • Deploy with Reserved VM for scheduled report generators that must run at specific times (weekly summaries, monthly invoices); use Autoscale for web apps that respond to user requests.
  • Cache Reports API responses for at least 15 minutes β€” Toggl data has some processing delay and repeated identical queries waste API capacity.

Alternatives

Frequently asked questions

How do I store my Toggl API token in Replit?

Click the lock icon πŸ”’ in the left sidebar of your Replit project to open the Secrets pane. Add TOGGL_API_TOKEN with your 32-character API token from Toggl Profile Settings > API Token, and TOGGL_WORKSPACE_ID with your numeric workspace ID. Access them in Python with os.environ['TOGGL_API_TOKEN'] and in Node.js with process.env.TOGGL_API_TOKEN.

Why does Toggl use 'api_token' as the password in Basic Auth?

This is a historical design choice by Toggl that predates modern API key authentication standards. The Basic Auth format (username:password) was repurposed: the API token serves as the username, and the literal string 'api_token' is a fixed password placeholder. This pattern works for both personal API tokens and OAuth access tokens β€” the password field is always 'api_token' regardless of which credential type you use.

Can I use the Toggl API on the free tier?

Yes. Toggl Track's free plan includes full API access for the main Toggl API and basic Reports API functionality. Advanced reporting features (like team analytics across multiple users) require a paid Starter or Premium plan. The free tier is fully functional for single-user integrations and personal time tracking automation.

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

The main Toggl API (api.track.toggl.com/api/v9/) handles CRUD operations: creating time entries, managing projects and clients, and reading raw time entry data. The Reports API (reports.track.toggl.com/api/v3/) provides pre-aggregated analytics: summary totals by project/user, detailed filterable entry lists, and weekly distributions. For billing and analytics use the Reports API; for creating and modifying time data use the main API.

How do I represent a running timer in the Toggl API?

Toggl represents running (in-progress) timers with a negative duration: duration = -(Unix timestamp of start time in seconds). This lets Toggl calculate elapsed time as: time.now() + duration (since duration is negative, this subtraction gives elapsed seconds). When creating a running timer via the API, set duration to -int(time.time()) at the moment you start it. To stop it, use the /stop endpoint which sets the end time and calculates the final positive duration.

Does Replit work with Toggl Track webhooks?

Toggl Track supports webhooks on paid plans (Starter and above). After deploying your Replit app to get a stable URL, register your webhook endpoint in Toggl under Workspace Settings > Webhooks. Toggl can notify your Replit app when time entries are created, stopped, or deleted. Webhooks require a deployed URL β€” development preview URLs will not work for production webhooks.

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.