Toggl Track API rate limits explained
How many requests you get, what happens when you run out, and how to write a script or integration that never gets cut off halfway.
The limits in one table
Toggl introduced these limits on September 5, 2025 (Toggl Knowledge Base).
| What | Limit | When you hit it |
|---|---|---|
| Requests to a workspace or organization, Toggl Free | 30 per hour, per user, per organization | HTTP 402 |
| Same, Toggl Starter | 240 per hour, per user, per organization | |
| Same, Toggl Premium | 600 per hour, per user, per organization | |
| Same, Toggl Enterprise | Higher, custom limits | HTTP 402 |
User-specific requests (e.g. /api/v9/me) | 30 per hour, per user | HTTP 402 |
| Short-term rate limit (leaky bucket) | "a safe window will be 1 request per second", per API token per IP | HTTP 429 |
| Webhooks, per workspace | Free: 1 webhook (up to 3 events). Starter: 2 (up to 6 events each). Premium: 3 (up to 12 events each). | – |
Sources: API & Webhook limits and Toggl API docs: API Quota and Rate limits.
Which limit applies to which request
- Workspace/organization requests are requests "that point to a specific workspace or organization (e.g., fetching reports, time entries, or projects for a workspace)".
Their quota depends on the organization's plan (Toggl). Paths like
/api/v9/workspaces/{id}/projectsand the Reports API (/reports/api/v3/workspace/{id}/…) fall in this group. - User-specific requests are about your own user data and don't target a workspace. Toggl's example is
/api/v9/me. They have their own 30-per-hour budget, separate from the workspace one. - Per user, per organization means each person has their own quota, and someone in two organizations has a separate quota in each. Ten people using the same integration don't share one bucket of 30 (Toggl API docs).
- The 1-request-per-second leaky bucket applies "per API token per IP", so two users from the same IP each get their own allocation (Toggl API docs).
How the hourly window works
Toggl calls it a sliding window. The Knowledge Base puts it like this: the count begins with your first request and lasts 60 minutes, then resets (Toggl). You don't have to track that yourself, because Toggl sends two response headers (Toggl API docs):
| Header | Meaning |
|---|---|
X-Toggl-Quota-Remaining | How many requests you have left in the current window |
X-Toggl-Quota-Resets-In | How many seconds until the current window resets |
Read them on every response. When Remaining gets low, stop and come back after Resets-In seconds.
402 vs 429 (and other status codes)
| Status | Meaning | What to do (Toggl's guidance) |
|---|---|---|
| 402 | Hourly quota used up. Also used for "workspace should be upgraded to have access to said feature". | Stop. For the quota, wait for the window to reset. Toggl suggests exponential backoff and suspending requests when you see 402. For a plan feature, don't repeat the request until the plan changes. |
| 429 | Too many requests in a short time (leaky bucket) | "Back off for a few minutes". A rate of 1 request per second should then be available. |
| 4xx (other) | Something is wrong with the request | Don't retry the same payload. Read the response body, which is usually a readable message. |
| 5xx | Server error | Wait a random delay before the next request |
| 410 | Gone | Don't call that endpoint again |
Sources: Toggl API docs: Generic responses and API & Webhook limits.
Because 402 means two different things, look at the quota headers and the response body to tell them apart.
(That's our approach, not something Toggl documents. If Remaining is 0 it's almost certainly the quota.)
What 30 requests an hour gets you
The Reports API detailed report returns 50 time entries per page by default, and the X-Next-Row-Number response header tells you where the next page starts
(Toggl Reports API). So, as rough arithmetic for a Toggl Free workspace:
- 30 requests × 50 entries ≈ 1,500 entries per hour at most, minus any requests you spend on projects, clients or tags.
- Hypothetical example: someone who logs 10 entries a working day has about 220 entries a month, which is 5 pages, so 5 requests.
- A long history (say 10,000 entries) needs about 200 requests: roughly 7 hours on Free, under an hour on Starter, or about 20 minutes on Premium, if nothing else uses the quota.
Polling the "list time entries" endpoint every few minutes, one request per entry, or fetching all projects on every run burns through the quota much faster.
Practical tips
- Fetch in bulk. Use the Reports API detailed report (50 entries per request) rather than one request per entry.
- Cache what rarely changes. Project, client and tag names don't need fetching on every run. Toggl itself recommends caching (Toggl).
- Pace requests. Keep at least about a second between calls to stay under the leaky bucket.
- Read the quota headers and keep a reserve. Stop when
X-Toggl-Quota-Remainingis down to 1 or 2, so a later "check my token" call still works. - Make long syncs resumable. Save the next page's row number, stop, and continue after
X-Toggl-Quota-Resets-Inseconds instead of starting over. - Back off on 429 and 5xx. Retry a few times with growing, slightly random delays. If it keeps happening, stop and try again in a few minutes, as Toggl suggests.
- Don't over-schedule. For most reporting, an hourly or daily refresh is plenty. Toggl explicitly asks people to reduce how often tools like Zapier run (Toggl).
- Mind Apps Script's own limits. One run can last 6 minutes, and on a consumer account triggers get 90 minutes of total runtime a day (Google).
Time spent in
Utilities.sleepcounts toward both, so a paced sync of a big history needs several runs anyway.
An Apps Script that paces itself
A small, readable wrapper around UrlFetchApp.fetch that applies the tips above, plus an example that pages through the detailed report and
stops with a resume point when the quota is nearly gone. Toggl's API uses HTTP Basic auth with your API token as the username and the word api_token as the password.
/**
* A polite fetch for the Toggl Track API in Apps Script:
* - spaces requests at least 1.1 s apart (Toggl: ~1 request/second is safe)
* - reads X-Toggl-Quota-Remaining / X-Toggl-Quota-Resets-In
* - stops before the hourly quota runs out, and on HTTP 402
* - retries 429 and 5xx with exponential backoff plus jitter
* Returns {text, headers (lower-case names), stopForMinutes}.
*/
var TOGGL_RESERVE = 2; // leave a couple of requests for next time
var TOGGL_MIN_GAP_MS = 1100;
var lastTogglCallMs = 0;
function togglFetch(url, options) {
options = options || {};
options.muteHttpExceptions = true;
for (var attempt = 0; attempt < 5; attempt++) {
var wait = lastTogglCallMs + TOGGL_MIN_GAP_MS - Date.now();
if (wait > 0) Utilities.sleep(wait);
var res = UrlFetchApp.fetch(url, options);
lastTogglCallMs = Date.now();
var h = lowerCaseKeys(res.getAllHeaders());
var remaining = parseInt(h['x-toggl-quota-remaining'], 10);
var resetsIn = parseInt(h['x-toggl-quota-resets-in'], 10);
var code = res.getResponseCode();
if (code >= 200 && code < 300) {
var low = !isNaN(remaining) && remaining <= TOGGL_RESERVE;
return {
text: res.getContentText(),
headers: h,
// If the quota is nearly used up, tell the caller to stop for a while.
stopForMinutes: low ? Math.ceil((isNaN(resetsIn) ? 3600 : resetsIn) / 60) : 0
};
}
if (code === 402) {
// Hourly quota used up, or a feature your Toggl plan doesn't include.
throw new Error('Toggl HTTP 402. Quota resets in ' +
(isNaN(resetsIn) ? 'unknown' : Math.ceil(resetsIn / 60) + ' min') +
'. Body: ' + res.getContentText().slice(0, 200));
}
if (code === 429 || code >= 500) {
// Exponential backoff with jitter: about 2 s, 4 s, 8 s, 16 s.
Utilities.sleep(Math.pow(2, attempt + 1) * 1000 + Math.floor(Math.random() * 1000));
continue;
}
throw new Error('Toggl HTTP ' + code + ': ' + res.getContentText().slice(0, 200));
}
throw new Error('Toggl is still busy after 5 tries. Try again in a few minutes.');
}
function lowerCaseKeys(obj) {
var out = {};
Object.keys(obj || {}).forEach(function (k) { out[k.toLowerCase()] = obj[k]; });
return out;
}
/**
* Example: page through the Reports API v3 detailed report.
* 50 entries per page by default; X-Next-Row-Number says where the next page starts.
*/
function fetchDetailedReport(token, workspaceId, startDate, endDate) {
var url = 'https://api.track.toggl.com/reports/api/v3/workspace/' + workspaceId + '/search/time_entries';
var auth = 'Basic ' + Utilities.base64Encode(token + ':api_token');
var rows = [];
var body = { start_date: startDate, end_date: endDate };
for (var page = 0; page < 200; page++) {
var res = togglFetch(url, {
method: 'post',
contentType: 'application/json',
headers: { Authorization: auth },
payload: JSON.stringify(body)
});
rows = rows.concat(JSON.parse(res.text) || []);
var next = res.headers['x-next-row-number'];
if (!next) return { rows: rows, done: true };
body.first_row_number = parseInt(next, 10);
if (res.stopForMinutes) {
// Save body.first_row_number somewhere (e.g. PropertiesService) and resume later.
return { rows: rows, done: false, resumeFrom: body.first_row_number, waitMinutes: res.stopForMinutes };
}
}
return { rows: rows, done: false, resumeFrom: body.first_row_number };
}
We tested this against a simulated Toggl API (paging, a low quota, 429 retries, 402 and 5xx), not a live Toggl account. It's deliberately simple. For example, the pacing state only lasts for one run. Check its results against Toggl before you rely on it.
If you'd rather not maintain this yourself, our add-on Hourly Sheets does the same things (pacing, quota headers, pause and resume) with billing reports on top. It is not released yet. The free options are compared in 5 ways to get Toggl data into Google Sheets.
What the docs don't say (or say differently)
- Enterprise and the
/melimit. The Knowledge Base says users who are only members of an Enterprise organization have unlimited user-specific requests. The API docs say the 30-per-hour user limit applies regardless of plan, with no higher quotas available. We can't tell which is current. If it matters, ask Toggl. - Exactly which endpoints count as "user-specific". Toggl gives
/api/v9/meas the example. We assume other/me/…paths count the same way, but the docs don't list them. - The exact leaky-bucket size. Toggl only says limits "will and can change" and that 1 request per second is safe.
Sources
All read on 2026-10-05. Limits can change, so check Toggl's pages if you're building something that depends on them.
- Toggl Knowledge Base: API & Webhook limits
- Toggl API documentation: overview (API Quota, Rate limits, Generic responses)
- Toggl Reports API overview (pagination) · Detailed reports endpoint
- Google Apps Script quotas
Found something wrong or out of date? Tell us via Support.