Connect your systems to TimeTac
A RESTful API for reading and writing time tracking, absences, projects and people in a TimeTac account. Every account is isolated: your credentials only ever reach your own company's data.
Quickstart
Three requests from nothing to reading data. Replace
{account} with your TimeTac account name — the same name you
use to sign in. Type it into Account in the bar above and every example on
this page, including the ones in the reference, switches to your account.
-
Create API credentials
In TimeTac, create a client ID and client secret for your integration. These identify your application and are scoped to your account only.
-
Exchange them for an access token
The API uses OAuth 2.0 client credentials. Tokens are short-lived — read
expires_inand refresh before it elapses.# returns { "access_token": "...", "token_type": "bearer", "expires_in": 3600 } curl -X POST 'https://sandbox-plus-api.timetac.com/{account}/auth/oauth2/token' \ -d 'grant_type=client_credentials' \ -d 'client_id=YOUR_CLIENT_ID' \ -d 'client_secret=YOUR_CLIENT_SECRET'
const res = await fetch('https://sandbox-plus-api.timetac.com/{account}/auth/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: 'YOUR_CLIENT_ID', client_secret: 'YOUR_CLIENT_SECRET', }), }); const { access_token } = await res.json();
import requests res = requests.post( 'https://sandbox-plus-api.timetac.com/{account}/auth/oauth2/token', data={ 'grant_type': 'client_credentials', 'client_id': 'YOUR_CLIENT_ID', 'client_secret': 'YOUR_CLIENT_SECRET', }, ) token = res.json()['access_token']<?php $ch = curl_init('https://sandbox-plus-api.timetac.com/{account}/auth/oauth2/token'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => http_build_query([ 'grant_type' => 'client_credentials', 'client_id' => 'YOUR_CLIENT_ID', 'client_secret' => 'YOUR_CLIENT_SECRET', ]), ]); $token = json_decode(curl_exec($ch), true)['access_token'];
-
Call an endpoint
Send the token as a bearer header. Actions are part of the path rather than the HTTP verb, so a read is
/timeTrackings/read/.curl 'https://sandbox-plus-api.timetac.com/{account}/v6/timeTrackings/read/?_limit=10' \ -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
const res = await fetch('https://sandbox-plus-api.timetac.com/{account}/v6/timeTrackings/read/?_limit=10', { headers: { Authorization: `Bearer ${token}` }, }); const { Results } = await res.json();import requests res = requests.get( 'https://sandbox-plus-api.timetac.com/{account}/v6/timeTrackings/read/', params={'_limit': 10}, headers={'Authorization': f'Bearer {token}'}, ) results = res.json()['Results']<?php $ch = curl_init('https://sandbox-plus-api.timetac.com/{account}/v6/timeTrackings/read/?_limit=10'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$token], ]); $results = json_decode(curl_exec($ch), true)['Results'];
How requests are shaped
Two conventions carry across every resource. Learn them once and the remaining 179 endpoints behave predictably.
URL structure
The account comes first, then the version, then the resource and its action.
https://{environment}.timetac.com/{account}/v6/{resource}/{action}/
| {environment} | sandbox-plus-api or prod-plus-api |
| {account} | Your TimeTac account name |
| v6 | API version |
| {resource} | For example timeTrackings |
| {action} | read, create, update, delete |
userapi path segment belongs
to v1–v5 only and is not used here.Response envelope
Every successful response is wrapped;
the payload is always the Results array.
{
"Success": true,
"NumResults": 2,
"ResourceName": "timeTrackings",
"ServerTimeZone": "Europe/Vienna",
"Results": [
{ "id": 1 },
{ "id": 2 }
]
}
Reading, paging and filtering
Every read endpoint accepts the same control
parameters. They all start with an underscore, which is what separates them from the
resource's own fields.
Paging and sorting
| Parameter | Purpose |
|---|---|
| _limit | Maximum rows to return. Defaults to 100. |
| _offset | Rows to skip. Combine with _limit to page. |
| _include_total_num_results | Set to 1 to add TotalNumResults to the envelope — the full count ignoring _limit, which is what you need to build a pager. |
| _order_by | Field name to sort by. |
| _sort_direction | ASC or DESC. |
Filtering
Any field an endpoint returns can be used as a filter. On its own a field means
"equals"; pair it with _op__<field> to choose a different
comparison.
# time trackings that started on or after 1 January, newest first GET /v6/timeTrackings/read/?start_time=2026-01-01&_op__start_time=gteq &_order_by=start_time&_sort_direction=DESC
| Operator | Meaning | Operator | Meaning |
|---|---|---|---|
| eq | Equals (the default) | like | SQL LIKE pattern match |
| neq | Not equal | in | In a comma-separated list |
| gt | Greater than | nin | Not in a comma-separated list |
| gteq | Greater than or equal | betw | Between two comma-separated bounds |
| lt | Less than | null | Field is null |
| lteq | Less than or equal | notnull | Field is not null |
Shaping the response
| Parameter | Purpose |
|---|---|
| _fields | Comma-separated list of fields to return, instead of the full record. Worth using on wide resources. |
| _resolve | Comma-separated relations to expand inline, so you do not have to follow ids with extra calls. Resolved values come back under keys joined by __resolved__ — for example user_id__resolved__name. |
Staying in sync
Rather than re-reading everything, ask for what changed. _since
and _until bound the window by last-modified time, and
_fetch_deleted is what makes deletions visible — without it a
removed record simply stops appearing and your copy keeps a row that no longer exists.
| Parameter | Purpose |
|---|---|
| _since | Only rows modified at or after this date-time. |
| _until | Only rows modified up to this date-time. |
| _fetch_deleted | Set to 1 to include records deleted since _since. Supported on resources that track hard deletions. |
The deltaSync resource does this across
many resources in one call — see it in the reference.
When a call fails
| Status | Meaning |
|---|---|
| 400 | Malformed request — a required parameter is missing or unparseable. |
| 401 | Missing, expired or invalid access token. |
| 403 | Authenticated, but not permitted for this record. |
| 410 | The resource is gone. |
| 422 | Well-formed, but a value failed validation. |
| 487 | Account is migrating or in maintenance — retry later. |
Error responses carry a
machine-readable code alongside the status; the full set is the
ErrorCode schema in the reference.
Plans & features
Parts of the API are gated by the features your plan includes, and some features additionally require a particular seat on the user making the call. Endpoints in the reference carry a badge naming the feature they need.
/v6/features/read/ — it answers per feature with
enabled, the applicable limit and the seat types the
feature requires, resolved against your current plan.Availability is enforced per field as often as per endpoint: an endpoint may answer without the feature while rejecting particular fields or values. The badge names the feature and says which of the two applies, so treat it as "this needs feature X", not as "this endpoint is blocked".
Resources
74 resources, grouped by what they do. Select one to open it in the reference.
Time tracking
10 resources · 23 endpointsTimesheets
4 resources · 6 endpointsAbsences & leave
10 resources · 27 endpointsPeople & organisation
8 resources · 33 endpointsProjects, tasks & budgets
10 resources · 29 endpointsWork schedules
4 resources · 14 endpointsAuthentication & permissions
14 resources · 24 endpoints- authentication
- oAuthClients
- permissionResolveAbsenceTypesAndUsers
- permissionResolveDepartments
- permissionResolveEntities
- permissionResolveOauthClients
- permissionResolveProjectsAndTasks
- permissionResolveQuestions
- permissionResolveUsers
- permissions
- permissionScopes
- permissionsProjectsAndTasksPermissions
- ssoConfig
- userToOauthClients