GET List timesheets visible to the caller
https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets
Returns the timesheets the caller may see whose period overlaps the required date_from/date_to window; a period is returned when it intersects the window at either edge, so the leading period is included even though it starts before date_from. The window may span at most 90 days (date_to no more than 90 days after date_from); a wider window is rejected with a 400 list_window_too_large error. Scope is every setup in the workspace for an admin with manage_timesheet_approvals, otherwise the setups the caller approves, or — for a member who approves nothing — their own; an explicit setup_id narrows to that one setup. Discontinued setups (end_date in the past) are excluded from this scope, but an explicit setup_id still resolves one, so retained history stays readable. Within a setup an admin or approver sees every member, a plain member only their own rows. Periods that have never been submitted are included as not-submitted rows; statuses narrows the result to the given approval states.
- cURL
- Go
- Ruby
- JavaScript
- Python
- Rust
curl https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets \
-H "Content-Type: application/json" \
-u <email>:<password>
req, err := http.NewRequest(http.MethodGet,
"https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets")
if err != nil {
print(err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.SetBasicAuth("<email>", "<password>")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
print(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
print(err)
}
fmt.Print(string(body))
require 'net/http'
require 'uri'
require 'json'
uri = URI('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets')
req = Net::HTTP::Get.new(uri)
req['Content-Type'] = "application/json"
req.basic_auth '<email>', '<password>'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(req)
end
puts JSON.parse(res.body)
fetch("https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Basic ${base64.encode(<email>:<password>)}`
},
})
.then((resp) => resp.json())
.then((json) => {
console.log(json);
})
.catch(err => console.error(err));
import requests
from base64 import b64encode
data = requests.get('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets', headers={'content-type': 'application/json', 'Authorization' : 'Basic %s' % b64encode(b"<email>:<password>").decode("ascii")})
print(data.json())
extern crate tokio;
extern crate serde_json;
use reqwest::{Client};
use reqwest::header::{CONTENT_TYPE};
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let client = Client::new().basic_auth("<email>", "<password>");
let json = client.request(Method::GET, "https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets".to_string())
.header(CONTENT_TYPE, "application/json")
.send()
.await?
.json()
.await?;
println!("{:#?}", json);
Ok(())
}
Parameters
Path
| name | type | required | description |
|---|
| organization_id | integer | true | organization ID |
| workspace_id | integer | true | workspace ID |
Query
| name | type | required | description |
|---|
| setup_id | integer | false | filter to a single setup |
| user_account_ids | []integer | false | filter to members, comma-separated or repeated |
| approver_user_account_ids | []integer | false | filter to setups staffed by these approvers, comma-separated or repeated |
| team_ids | []integer | false | filter to members of these teams, comma-separated or repeated |
| current_layer_approver_user_account_id | integer | false | filter to timesheets whose current approval layer is staffed by this approver (their turn to act); combine with statuses=submitted for a pending-review view |
| statuses | []string | false | filter to statuses, comma-separated or repeated (not_submitted, submitted, approved, changes_requested) |
| date_from | string | true | inclusive start of the window; every period overlapping it is returned (YYYY-MM-DD) |
| date_to | string | true | inclusive end of the window, at most 90 days after date_from; every period overlapping it is returned (YYYY-MM-DD) |
| page | integer | false | page number |
| per_page | integer | false | page size |
Response
200
| Name | Type | Description |
|---|
| items | Array of object | - |
items
| Name | Type | Description |
|---|
| approvers | Array of object | Approvers are the approvers responsible for reviewing this timesheet: the setup's configured approvers at the timesheet's current layer. Always populated. |
| capacity_minutes | integer | CapacityMinutes is the member's working-capacity minutes for the period — the expected hours LoggedMinutes is measured against. |
| flag_count | integer | FlagCount is the number of active flags on this timesheet. |
| logged_minutes | integer | LoggedMinutes is the sum of durations for entries whose start falls in the period (in the member's timezone); an entry that overlaps but starts in an adjacent period is not counted here. |
| member_name | string | MemberName is the member's display name, resolved from their account profile at query time. Not persisted with the timesheet, so it always reflects the member's current name. |
| reviews | Array of object | Reviews is the name-resolved review trail (actor, action, time), oldest first, so the most recent entry is who last acted on the timesheet. Empty for virtual rows. |
| timesheet | object | Timesheet is the underlying record (virtual or persisted). |
approvers
| Name | Type | Description |
|---|
| name | string | Name is the approver's display name, resolved from their account profile at query time. |
| user_account_id | integer | UserAccountID identifies the approver. |
reviews
| Name | Type | Description |
|---|
| action | string | Action is the review action taken. Valid values: submitted, withdrawn, approved, changes_requested, resubmitted, approval_withdrawn, admin_approved, admin_changes_requested. |
| created_at | string | CreatedAt is when the action was taken. |
| name | string | Name is the actor's display name, resolved from their account profile at query time. |
| user_account_id | integer | UserAccountID is the actor who performed the action. |
timesheet
| Name | Type | Description |
|---|
| created_at | string | CreatedAt is the row creation timestamp. |
| current_layer | integer | CurrentLayer is the layer that must act next for the timesheet to advance. On submission it is 0 (waiting on the lowest approver layer). After the approver at the current layer approves, it advances to the next configured layer. Setup validation requires approver layers to be contiguous from 0, so in practice that next layer is N+1; the traversal (nextLayerAfter) nonetheless does not assume contiguity. The timesheet reaches Status=approved when no higher configured layer remains to act. When an approver at layer N requests changes, status becomes changes_requested with CurrentLayer=N. On resubmit, status returns to submitted with CurrentLayer=N — the chain restarts at the rejecting layer rather than at 0, preserving earlier-layer approvals. |
| deleted_at | string | DeletedAt soft-deletes the timesheet. While set, the row is excluded from all read and materialisation paths. Set by the per-member "Delete data" action; nil means live. |
| end_date | string | EndDate is the inclusive end of this period. |
| id | integer | ID is the surrogate key. |
| reviewed_at | string | ReviewedAt is the timestamp of the most recent review action (approve, request-changes, withdraw, admin override). |
| setup_id | integer | SetupID references the parent TimesheetSetup, which determines the period boundaries and the approval chain. |
| start_date | string | StartDate is the inclusive start of this period. Must align with the setup's period boundaries (validated on write). |
| status | string | Status is the current lifecycle state. Valid values: not_submitted, submitted, approved, changes_requested. |
| submitted_at | string | SubmittedAt is when the member most recently submitted (or resubmitted) the timesheet. Nil for not_submitted rows. |
| submitter_timezone | string | SubmitterTimezone is a snapshot of the member's IANA timezone at submission time. Used so approvers see the member's data in the member's local time even if the member later changes timezone. |
| updated_at | string | UpdatedAt tracks the last mutation. |
| user_account_id | integer | UserAccountID identifies the member whose timesheet this is. |
| workspace_id | integer | WorkspaceID is the workspace the timesheet belongs to. |
400
Bad Request
500
Internal Server Error
GET List the approvers the caller can filter the approvals list by
https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/approvers
Returns the approvers staffing the timesheet setups visible to the caller, each with their account ID and display name. These are the values accepted by the approver_user_account_ids filter when listing timesheets.
- cURL
- Go
- Ruby
- JavaScript
- Python
- Rust
curl https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/approvers \
-H "Content-Type: application/json" \
-u <email>:<password>
req, err := http.NewRequest(http.MethodGet,
"https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/approvers")
if err != nil {
print(err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.SetBasicAuth("<email>", "<password>")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
print(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
print(err)
}
fmt.Print(string(body))
require 'net/http'
require 'uri'
require 'json'
uri = URI('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/approvers')
req = Net::HTTP::Get.new(uri)
req['Content-Type'] = "application/json"
req.basic_auth '<email>', '<password>'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(req)
end
puts JSON.parse(res.body)
fetch("https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/approvers", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Basic ${base64.encode(<email>:<password>)}`
},
})
.then((resp) => resp.json())
.then((json) => {
console.log(json);
})
.catch(err => console.error(err));
import requests
from base64 import b64encode
data = requests.get('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/approvers', headers={'content-type': 'application/json', 'Authorization' : 'Basic %s' % b64encode(b"<email>:<password>").decode("ascii")})
print(data.json())
extern crate tokio;
extern crate serde_json;
use reqwest::{Client};
use reqwest::header::{CONTENT_TYPE};
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let client = Client::new().basic_auth("<email>", "<password>");
let json = client.request(Method::GET, "https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/approvers".to_string())
.header(CONTENT_TYPE, "application/json")
.send()
.await?
.json()
.await?;
println!("{:#?}", json);
Ok(())
}
Parameters
Path
| name | type | required | description |
|---|
| organization_id | integer | true | organization ID |
| workspace_id | integer | true | workspace ID |
Response
200
| Name | Type | Description |
|---|
| items | Array of object | - |
items
| Name | Type | Description |
|---|
| name | string | Name is the approver's display name, resolved from their account profile at query time. |
| user_account_id | integer | UserAccountID identifies the approver. |
500
Internal Server Error
GET Get the full detail view of a timesheet
https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}
Returns everything needed to review one timesheet in a single call: the record and its parent setup, the ordered approval chain, the member it belongs to, the time entries whose start falls in the period (enriched with their task and project), the active flags on those entries, and the hours summary. Visible to the member, to any approver in the setup's chain, and to workspace admins with manage_timesheet_approvals.
- cURL
- Go
- Ruby
- JavaScript
- Python
- Rust
curl https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id} \
-H "Content-Type: application/json" \
-u <email>:<password>
req, err := http.NewRequest(http.MethodGet,
"https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}")
if err != nil {
print(err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.SetBasicAuth("<email>", "<password>")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
print(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
print(err)
}
fmt.Print(string(body))
require 'net/http'
require 'uri'
require 'json'
uri = URI('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}')
req = Net::HTTP::Get.new(uri)
req['Content-Type'] = "application/json"
req.basic_auth '<email>', '<password>'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(req)
end
puts JSON.parse(res.body)
fetch("https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Basic ${base64.encode(<email>:<password>)}`
},
})
.then((resp) => resp.json())
.then((json) => {
console.log(json);
})
.catch(err => console.error(err));
import requests
from base64 import b64encode
data = requests.get('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}', headers={'content-type': 'application/json', 'Authorization' : 'Basic %s' % b64encode(b"<email>:<password>").decode("ascii")})
print(data.json())
extern crate tokio;
extern crate serde_json;
use reqwest::{Client};
use reqwest::header::{CONTENT_TYPE};
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let client = Client::new().basic_auth("<email>", "<password>");
let json = client.request(Method::GET, "https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}".to_string())
.header(CONTENT_TYPE, "application/json")
.send()
.await?
.json()
.await?;
println!("{:#?}", json);
Ok(())
}
Parameters
Path
| name | type | required | description |
|---|
| organization_id | integer | true | organization ID |
| workspace_id | integer | true | workspace ID |
| setup_id | integer | true | setup ID |
| start_date | string | true | period start (YYYY-MM-DD) |
| user_account_id | integer | true | member user account ID |
Response
200
| Name | Type | Description |
|---|
| approvers | Array of object | Approvers is the ordered approval chain at query time. |
| flags | Array of object | Flags are the active flags on those entries. |
| hours | object | Hours is the computed hours summary for the period. |
| member | object | Member is the snapshotted member info (name, email, timezone). |
| setup | object | Setup is the parent setup at query time. |
| time_entries | Array of object | TimeEntries are the entries whose start falls in the period (in the member's timezone), matching what is counted and what can be flagged - not every entry overlapping the window. Each is enriched with its task (name, tags) and project (name, color, client) so the detail view renders without extra lookups. |
| timesheet | object | Timesheet is the underlying record. |
approvers
| Name | Type | Description |
|---|
| created_at | string | CreatedAt is the row creation timestamp. |
| deleted_at | string | DeletedAt soft-deletes the approver assignment. While set, the row is treated as if the approver was never assigned. |
| id | integer | ID is the surrogate key. |
| layer | integer | Layer is the 0-indexed tier of this approver in the approval chain. A layer may have multiple approvers (multiple rows sharing the same setup_id and layer); any one of them acting is enough to advance the timesheet to the next layer. |
| setup_id | integer | SetupID references the parent TimesheetSetup. |
| user_account_id | integer | UserAccountID is the approver's account identifier. |
| workspace_id | integer | WorkspaceID is the workspace this approver's setup belongs to. |
flags
| Name | Type | Description |
|---|
| approver_user_account_id | integer | ApproverUserAccountID is the user who created the flag. |
| comment | string | Comment is the approver's note. Required and non-empty. |
| created_at | string | CreatedAt is the row creation timestamp. |
| deleted_at | string | DeletedAt soft-deletes the flag. Set when the member resubmits, when the timesheet is approved, or when the underlying time entry is deleted. |
| id | integer | ID is the surrogate key. |
| time_entry_id | integer | TimeEntryID references the time entry being flagged. The time entry must fall within the timesheet's period. |
| timesheet_id | integer | TimesheetID references the parent Timesheet. |
hours
| Name | Type | Description |
|---|
| billable_minutes | integer | BillableMinutes is the sum of durations on entries marked billable. |
| billable_percent | number | BillablePercent is BillableMinutes / TotalMinutes * 100, rounded to one decimal place. Zero when TotalMinutes is zero. |
| expected_minutes | integer | ExpectedMinutes is the working capacity for the period, sourced from the capacity service. |
| logged_minutes | integer | LoggedMinutes equals TotalMinutes; kept as a separate field so the planned-vs-logged comparison is self-documenting at the call site. |
| planned_minutes | integer | PlannedMinutes is the sum of time block durations planned for the period. |
| total_minutes | integer | TotalMinutes is the sum of logged time entry durations. |
member
| Name | Type | Description |
|---|
| email | string | Email is the member's email address, resolved from their account profile at query time. Not persisted with the timesheet. |
| name | string | Name is the member's display name, resolved from their account profile at query time. Not persisted with the timesheet, so it always reflects the member's current name. |
| timezone | string | Timezone is the IANA timezone snapshotted at submission and read back from Timesheet.SubmitterTimezone. |
| user_account_id | integer | UserAccountID identifies the member. |
setup
| Name | Type | Description |
|---|
| created_at | string | CreatedAt is the row creation timestamp. |
| deleted_at | string | DeletedAt soft-deletes the setup. While set, the setup is excluded from active queries and no new periods are generated for it. |
| email_reminder_enabled | boolean | EmailReminderEnabled gates the pending-reminder email channel for this setup. When false (the default) the reminder cron skips this setup's members even if ReminderDay/ReminderTime are set. |
| end_date | string | EndDate is set when the setup is discontinued. While nil, the setup continues to generate periods indefinitely. When set, no new periods are generated past this date. |
| id | integer | ID is the surrogate key. |
| period_days | integer | PeriodDays is the length of a period in days. Required (and minimum 1) when Periodicity == PeriodicityDays. Must be nil otherwise. |
| periodicity | string | Periodicity defines the cadence of the timesheet periods. See the Periodicity constants for valid values. Valid values: weekly, monthly, days. |
| reminder_day | integer | ReminderDay defines when in the period a reminder is sent to members who haven't submitted yet. Interpretation depends on Periodicity: - weekly: 0..6 (0 = Sunday) - monthly: 1..31 (clamps to month length) - days: number of days into the period Reminders are not sent when ReminderDay or ReminderTime is nil. |
| reminder_time | string | ReminderTime is the wall-clock time the reminder is sent, formatted HH:MM, interpreted in each member's own timezone (resolved when the reminder cron runs; UTC when the member's timezone is unknown). Reminders are not sent when ReminderDay or ReminderTime is nil. |
| start_date | string | StartDate is the anchor for period computation and the inclusive start of the very first period. All subsequent period boundaries derive from this date plus the periodicity rule. |
| updated_at | string | UpdatedAt tracks the last mutation. Nil for never-updated rows. |
| workspace_id | integer | WorkspaceID is the workspace this setup belongs to. |
time_entries
| Name | Type | Description |
|---|
| archived_at | string | - |
| billable | boolean | - |
| billable_source | string | Valid values: manual, task_default. |
| calendar_event | object | - |
| calendar_event_id | integer | - |
| created_at | string | - |
| deleted_at | string | - |
| description | string | - |
| duration | integer | - |
| id | integer | - |
| planned_at | string | - |
| planned_duration | integer | - |
| planned_start | string | - |
| project | object | - |
| project_id | integer | - |
| start | string | - |
| tag_ids | Array of integer | TagIDs is the entry's manual tag override. NULL (nil) means no override: the effective tags fall through to the linked task's tags. A non-nil value — including an empty array — is entry-authoritative. |
| tags | Array of object | Tags is the hydrated effective tag set: the override when TagIDs is set, else the linked task's active tags. Read-only. |
| task | object | - |
| task_id | integer | null |
| time_block_id | integer | - |
| timezone | string | Timezone is the effective IANA timezone hydrated on reads: the workspace timezone snapshotted at creation when set, else the creator's snapshot; NULL when unresolved. |
| toggl_user_id | integer | - |
| tracked_at | string | - |
| type | string | The type of time entry: either an activity or a break. Valid values: activity, break. |
| updated_at | string | - |
| workspace_id | integer | - |
calendar_event
| Name | Type | Description |
|---|
| all_day | boolean | - |
| background_color | string | null |
| end_time | string | - |
| foreground_color | string | null |
| html_link | string | - |
| id | integer | - |
| meeting_link | string | null |
| provider | string | - |
| start_time | string | - |
| title | string | - |
project
| Name | Type | Description |
|---|
| archived_at | string | - |
| client | object | - |
| color | string | - |
| custom_field_values | Array of object | CustomFieldValues are the parent project's CF values, hydrated by callers that surface them (currently the task list, via task.service.hydrateTaskProjectCustomFieldValues which routes through customfield.Service.GetFieldsByIDs and respects the PermissionViewWorkspaceProjectCustomFields gate). Producers that don't hydrate (e.g. timeentry) leave the slice empty; omitempty hides it on those responses. |
| draft | boolean | - |
| id | integer | - |
| is_template | boolean | - |
| name | string | - |
| permissions | Array of string | - |
| private | boolean | - |
| rate | object | - |
client
| Name | Type | Description |
|---|
| id | integer | - |
| name | string | - |
custom_field_values
| Name | Type | Description |
|---|
| custom_field_id | integer | - |
| custom_field_name | string | - |
| field_type | string | - |
| selected_options | Array of object | - |
| value | object | - |
selected_options
| Name | Type | Description |
|---|
| is_deleted | boolean | - |
| option_id | integer | - |
| option_name | string | - |
rate
| Name | Type | Description |
|---|
| billable | boolean | - |
| currency | string | - |
| end_at | string | - |
| has_more_rates | boolean | - |
| hourly_rate | number | - |
| project_color | string | - |
| project_created_at | string | - |
| project_id | integer | - |
| project_name | string | - |
| project_rate_id | integer | - |
| start_at | string | - |
| workspace_rate_id | integer | - |
| Name | Type | Description |
|---|
| color | string | - |
| id | integer | - |
| name | string | - |
task
| Name | Type | Description |
|---|
| allocation_unit | string | Valid values: percent, flat. |
| archived_at | string | - |
| assignee_user_ids | Array of integer | - |
| assignees | Array of object | A unified column for all task entity assignments |
| auto_log_time | boolean | - |
| billable | boolean | - |
| client | object | - |
| color | string | - |
| created_at | string | - |
| custom_field_values | Array of object | - |
| deleted_at | string | - |
| description | string | - |
| end_date | string | - |
| estimate_type | string | Valid values: daily, total. |
| estimated_mins | integer | - |
| id | integer | - |
| is_template | boolean | - |
| metadata | object | - |
| name | string | - |
| notes | string | - |
| parent_task_id | integer | - |
| parent_task_name | string | - |
| pinned | boolean | - |
| position | integer | - |
| priority | string | Valid values: none, low, medium, high. |
| priority_at | string | - |
| private | boolean | - |
| project | object | - |
| project_id | integer | - |
| recurrence_date | string | - |
| recurring_task_id | integer | - |
| rrule | string | - |
| source | string | - |
| source_template_task_id | integer | - |
| start_date | string | - |
| status | object | - |
| status_id | integer | - |
| status_updated_at | string | - |
| tag_ids | Array of integer | - |
| tags | Array of object | - |
| toggl_user_id | integer | - |
| updated_at | string | - |
| workspace_id | integer | - |
assignees
| Name | Type | Description |
|---|
| id | integer | - |
| type | string | Valid values: user, team. |
client
| Name | Type | Description |
|---|
| id | integer | - |
| name | string | - |
custom_field_values
| Name | Type | Description |
|---|
| custom_field_id | integer | - |
| custom_field_name | string | - |
| field_type | string | - |
| selected_options | Array of object | - |
| value | object | - |
selected_options
| Name | Type | Description |
|---|
| is_deleted | boolean | - |
| option_id | integer | - |
| option_name | string | - |
| Name | Type | Description |
|---|
| all_day | boolean | - |
| calendar_event_id | integer | - |
| calendar_id | integer | - |
| extension_source | string | Browser-extension provenance: the source/config that produced the task and the page URL it was tracked from. Written by the extension's start-from-description flow; ExtensionURL is queryable via FindByURL. |
| extension_url | string | - |
| external_id | string | - |
| ical_uid | string | - |
| last_asserted_track_project_id | integer | LastAssertedTrackProjectID is the Track planned_task.project_id that the Focus→Track mirror last successfully wrote for this task. Track→Focus uses it to distinguish a stale echo (Track still holds this value after a Focus-side project move) from an intentional Track-side project move. Internal mirror state, written by toggl_api focus_sync_worker; not user-facing. |
| meeting_link | string | - |
| project_assignment | object | - |
| updated_at | string | - |
project_assignment
| Name | Type | Description |
|---|
| accuracy | number | - |
| confirmed_at | string | - |
| match_tier | string | Valid values: exact_name, similar_name. |
| matched_name | string | - |
| normalized_name | string | - |
| origin | string | Valid values: manual, auto_suggestion, confirmed_suggestion, integration, legacy, explicit_mapping. |
| suggested_at | string | - |
project
| Name | Type | Description |
|---|
| archived_at | string | - |
| client | object | - |
| color | string | - |
| custom_field_values | Array of object | CustomFieldValues are the parent project's CF values, hydrated by callers that surface them (currently the task list, via task.service.hydrateTaskProjectCustomFieldValues which routes through customfield.Service.GetFieldsByIDs and respects the PermissionViewWorkspaceProjectCustomFields gate). Producers that don't hydrate (e.g. timeentry) leave the slice empty; omitempty hides it on those responses. |
| draft | boolean | - |
| id | integer | - |
| is_template | boolean | - |
| name | string | - |
| permissions | Array of string | - |
| private | boolean | - |
| rate | object | - |
client
| Name | Type | Description |
|---|
| id | integer | - |
| name | string | - |
custom_field_values
| Name | Type | Description |
|---|
| custom_field_id | integer | - |
| custom_field_name | string | - |
| field_type | string | - |
| selected_options | Array of object | - |
| value | object | - |
rate
| Name | Type | Description |
|---|
| billable | boolean | - |
| currency | string | - |
| end_at | string | - |
| has_more_rates | boolean | - |
| hourly_rate | number | - |
| project_color | string | - |
| project_created_at | string | - |
| project_id | integer | - |
| project_name | string | - |
| project_rate_id | integer | - |
| start_at | string | - |
| workspace_rate_id | integer | - |
status
| Name | Type | Description |
|---|
| emoji | string | - |
| id | integer | - |
| name | string | - |
| type | string | Valid values: todo, done, in_progress, blocked. |
| Name | Type | Description |
|---|
| color | string | - |
| id | integer | - |
| name | string | - |
timesheet
| Name | Type | Description |
|---|
| created_at | string | CreatedAt is the row creation timestamp. |
| current_layer | integer | CurrentLayer is the layer that must act next for the timesheet to advance. On submission it is 0 (waiting on the lowest approver layer). After the approver at the current layer approves, it advances to the next configured layer. Setup validation requires approver layers to be contiguous from 0, so in practice that next layer is N+1; the traversal (nextLayerAfter) nonetheless does not assume contiguity. The timesheet reaches Status=approved when no higher configured layer remains to act. When an approver at layer N requests changes, status becomes changes_requested with CurrentLayer=N. On resubmit, status returns to submitted with CurrentLayer=N — the chain restarts at the rejecting layer rather than at 0, preserving earlier-layer approvals. |
| deleted_at | string | DeletedAt soft-deletes the timesheet. While set, the row is excluded from all read and materialisation paths. Set by the per-member "Delete data" action; nil means live. |
| end_date | string | EndDate is the inclusive end of this period. |
| id | integer | ID is the surrogate key. |
| reviewed_at | string | ReviewedAt is the timestamp of the most recent review action (approve, request-changes, withdraw, admin override). |
| setup_id | integer | SetupID references the parent TimesheetSetup, which determines the period boundaries and the approval chain. |
| start_date | string | StartDate is the inclusive start of this period. Must align with the setup's period boundaries (validated on write). |
| status | string | Status is the current lifecycle state. Valid values: not_submitted, submitted, approved, changes_requested. |
| submitted_at | string | SubmittedAt is when the member most recently submitted (or resubmitted) the timesheet. Nil for not_submitted rows. |
| submitter_timezone | string | SubmitterTimezone is a snapshot of the member's IANA timezone at submission time. Used so approvers see the member's data in the member's local time even if the member later changes timezone. |
| updated_at | string | UpdatedAt tracks the last mutation. |
| user_account_id | integer | UserAccountID identifies the member whose timesheet this is. |
| workspace_id | integer | WorkspaceID is the workspace the timesheet belongs to. |
400
Bad Request
403
Forbidden
404
Not Found
500
Internal Server Error
GET List the active flags on a timesheet
https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/flags
Returns the active flags on a timesheet — the individual time entries an approver marked as needing attention, each with the flagging approver and their comment. Flags clear when the member resubmits, when the timesheet is approved, or when the flagged entry is deleted, so an empty list means nothing is currently disputed.
- cURL
- Go
- Ruby
- JavaScript
- Python
- Rust
curl https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/flags \
-H "Content-Type: application/json" \
-u <email>:<password>
req, err := http.NewRequest(http.MethodGet,
"https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/flags")
if err != nil {
print(err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.SetBasicAuth("<email>", "<password>")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
print(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
print(err)
}
fmt.Print(string(body))
require 'net/http'
require 'uri'
require 'json'
uri = URI('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/flags')
req = Net::HTTP::Get.new(uri)
req['Content-Type'] = "application/json"
req.basic_auth '<email>', '<password>'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(req)
end
puts JSON.parse(res.body)
fetch("https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/flags", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Basic ${base64.encode(<email>:<password>)}`
},
})
.then((resp) => resp.json())
.then((json) => {
console.log(json);
})
.catch(err => console.error(err));
import requests
from base64 import b64encode
data = requests.get('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/flags', headers={'content-type': 'application/json', 'Authorization' : 'Basic %s' % b64encode(b"<email>:<password>").decode("ascii")})
print(data.json())
extern crate tokio;
extern crate serde_json;
use reqwest::{Client};
use reqwest::header::{CONTENT_TYPE};
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let client = Client::new().basic_auth("<email>", "<password>");
let json = client.request(Method::GET, "https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/flags".to_string())
.header(CONTENT_TYPE, "application/json")
.send()
.await?
.json()
.await?;
println!("{:#?}", json);
Ok(())
}
Parameters
Path
| name | type | required | description |
|---|
| organization_id | integer | true | organization ID |
| workspace_id | integer | true | workspace ID |
| setup_id | integer | true | setup ID |
| start_date | string | true | period start (YYYY-MM-DD) |
| user_account_id | integer | true | member user account ID |
Response
200
| Name | Type | Description |
|---|
| items | Array of object | - |
items
| Name | Type | Description |
|---|
| approver_user_account_id | integer | ApproverUserAccountID is the user who created the flag. |
| comment | string | Comment is the approver's note. Required and non-empty. |
| created_at | string | CreatedAt is the row creation timestamp. |
| deleted_at | string | DeletedAt soft-deletes the flag. Set when the member resubmits, when the timesheet is approved, or when the underlying time entry is deleted. |
| id | integer | ID is the surrogate key. |
| time_entry_id | integer | TimeEntryID references the time entry being flagged. The time entry must fall within the timesheet's period. |
| timesheet_id | integer | TimesheetID references the parent Timesheet. |
400
Bad Request
403
Forbidden
404
Not Found
500
Internal Server Error
GET Get the review audit trail of a timesheet
https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/history
Returns the review trail of a timesheet in chronological order — every submission, withdrawal, approval and change request — with the acting user, any comment left, and when it happened. Empty for a period that has never been submitted.
- cURL
- Go
- Ruby
- JavaScript
- Python
- Rust
curl https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/history \
-H "Content-Type: application/json" \
-u <email>:<password>
req, err := http.NewRequest(http.MethodGet,
"https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/history")
if err != nil {
print(err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.SetBasicAuth("<email>", "<password>")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
print(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
print(err)
}
fmt.Print(string(body))
require 'net/http'
require 'uri'
require 'json'
uri = URI('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/history')
req = Net::HTTP::Get.new(uri)
req['Content-Type'] = "application/json"
req.basic_auth '<email>', '<password>'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(req)
end
puts JSON.parse(res.body)
fetch("https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/history", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Basic ${base64.encode(<email>:<password>)}`
},
})
.then((resp) => resp.json())
.then((json) => {
console.log(json);
})
.catch(err => console.error(err));
import requests
from base64 import b64encode
data = requests.get('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/history', headers={'content-type': 'application/json', 'Authorization' : 'Basic %s' % b64encode(b"<email>:<password>").decode("ascii")})
print(data.json())
extern crate tokio;
extern crate serde_json;
use reqwest::{Client};
use reqwest::header::{CONTENT_TYPE};
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let client = Client::new().basic_auth("<email>", "<password>");
let json = client.request(Method::GET, "https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/history".to_string())
.header(CONTENT_TYPE, "application/json")
.send()
.await?
.json()
.await?;
println!("{:#?}", json);
Ok(())
}
Parameters
Path
| name | type | required | description |
|---|
| organization_id | integer | true | organization ID |
| workspace_id | integer | true | workspace ID |
| setup_id | integer | true | setup ID |
| start_date | string | true | period start (YYYY-MM-DD) |
| user_account_id | integer | true | member user account ID |
Response
200
| Name | Type | Description |
|---|
| items | Array of object | - |
items
| Name | Type | Description |
|---|
| action | string | Action is the action that was performed. Valid values: submitted, withdrawn, approved, changes_requested, resubmitted, approval_withdrawn, admin_approved, admin_changes_requested. |
| actor_user_account_id | integer | ActorUserAccountID is the user who performed the action. |
| comment | string | Comment carries the message attached to the action. Required for changes_requested and admin_changes_requested actions; optional for others. Capped at 1000 characters. |
| created_at | string | CreatedAt is the timestamp of the action. |
| id | integer | ID is the surrogate key. |
| layer | integer | Layer is the approver layer at which this action was taken. Nil for actions taken outside the approver chain (admin overrides and member submissions / withdrawals). |
| timesheet_id | integer | TimesheetID references the parent Timesheet. |
400
Bad Request
403
Forbidden
404
Not Found
500
Internal Server Error
GET Get the hours summary for a timesheet period
https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/hours
Returns the time totals for a timesheet's period: logged, billable (with its share of the total), planned, and the member's expected working capacity. Only entries whose start falls in the period, in the member's timezone, are counted — an entry that overlaps the boundary belongs to the period it starts in.
- cURL
- Go
- Ruby
- JavaScript
- Python
- Rust
curl https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/hours \
-H "Content-Type: application/json" \
-u <email>:<password>
req, err := http.NewRequest(http.MethodGet,
"https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/hours")
if err != nil {
print(err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.SetBasicAuth("<email>", "<password>")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
print(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
print(err)
}
fmt.Print(string(body))
require 'net/http'
require 'uri'
require 'json'
uri = URI('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/hours')
req = Net::HTTP::Get.new(uri)
req['Content-Type'] = "application/json"
req.basic_auth '<email>', '<password>'
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(req)
end
puts JSON.parse(res.body)
fetch("https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/hours", {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": `Basic ${base64.encode(<email>:<password>)}`
},
})
.then((resp) => resp.json())
.then((json) => {
console.log(json);
})
.catch(err => console.error(err));
import requests
from base64 import b64encode
data = requests.get('https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/hours', headers={'content-type': 'application/json', 'Authorization' : 'Basic %s' % b64encode(b"<email>:<password>").decode("ascii")})
print(data.json())
extern crate tokio;
extern crate serde_json;
use reqwest::{Client};
use reqwest::header::{CONTENT_TYPE};
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let client = Client::new().basic_auth("<email>", "<password>");
let json = client.request(Method::GET, "https://focus.toggl.com/api/organizations/{organization_id}/workspaces/{workspace_id}/timesheets/{setup_id}/{start_date}/{user_account_id}/hours".to_string())
.header(CONTENT_TYPE, "application/json")
.send()
.await?
.json()
.await?;
println!("{:#?}", json);
Ok(())
}
Parameters
Path
| name | type | required | description |
|---|
| organization_id | integer | true | organization ID |
| workspace_id | integer | true | workspace ID |
| setup_id | integer | true | setup ID |
| start_date | string | true | period start (YYYY-MM-DD) |
| user_account_id | integer | true | member user account ID |
Response
200
| Name | Type | Description |
|---|
| billable_minutes | integer | BillableMinutes is the sum of durations on entries marked billable. |
| billable_percent | number | BillablePercent is BillableMinutes / TotalMinutes * 100, rounded to one decimal place. Zero when TotalMinutes is zero. |
| expected_minutes | integer | ExpectedMinutes is the working capacity for the period, sourced from the capacity service. |
| logged_minutes | integer | LoggedMinutes equals TotalMinutes; kept as a separate field so the planned-vs-logged comparison is self-documenting at the call site. |
| planned_minutes | integer | PlannedMinutes is the sum of time block durations planned for the period. |
| total_minutes | integer | TotalMinutes is the sum of logged time entry durations. |
400
Bad Request
403
Forbidden
404
Not Found
500
Internal Server Error