Guide · Toggl Track API

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).

WhatLimitWhen you hit it
Requests to a workspace or organization, Toggl Free30 per hour, per user, per organizationHTTP 402
Same, Toggl Starter240 per hour, per user, per organization
Same, Toggl Premium600 per hour, per user, per organization
Same, Toggl EnterpriseHigher, custom limitsHTTP 402
User-specific requests (e.g. /api/v9/me)30 per hour, per userHTTP 402
Short-term rate limit (leaky bucket)"a safe window will be 1 request per second", per API token per IPHTTP 429
Webhooks, per workspaceFree: 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

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):

HeaderMeaning
X-Toggl-Quota-RemainingHow many requests you have left in the current window
X-Toggl-Quota-Resets-InHow 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)

StatusMeaningWhat to do (Toggl's guidance)
402Hourly 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.
429Too 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 requestDon't retry the same payload. Read the response body, which is usually a readable message.
5xxServer errorWait a random delay before the next request
410GoneDon'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:

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

  1. Fetch in bulk. Use the Reports API detailed report (50 entries per request) rather than one request per entry.
  2. Cache what rarely changes. Project, client and tag names don't need fetching on every run. Toggl itself recommends caching (Toggl).
  3. Pace requests. Keep at least about a second between calls to stay under the leaky bucket.
  4. Read the quota headers and keep a reserve. Stop when X-Toggl-Quota-Remaining is down to 1 or 2, so a later "check my token" call still works.
  5. Make long syncs resumable. Save the next page's row number, stop, and continue after X-Toggl-Quota-Resets-In seconds instead of starting over.
  6. 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.
  7. 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).
  8. 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.sleep counts 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)

Sources

All read on 2026-10-05. Limits can change, so check Toggl's pages if you're building something that depends on them.

Found something wrong or out of date? Tell us via Support.