MENU navbar-image

Introduction

Welcome to the Timing API reference.

If you prefer a more interactive presentation of the API, you can download our Postman Collection and open it in a tool like Postman or Paw.

We also offer three Siri shortcuts that demonstrate starting and stopping timers via the API:

The "Start Fixed Timing Timer" shortcut will ask you once for a combination of timer title and project name. From then on, it will always launch a shortcut with that combination of title and project. You can create several copies of this shortcut (e.g. by duplicating it), each with a different title/project combination.

Use Cases

Using AI Tools

If you prefer using an AI assistant to explore or interact with this API, we've provided a simplified version of the documentation in LLMs.txt. This file is designed to be easily parsed by language models and other AI tools, making it easier to surface relevant information quickly.

You can load the contents of LLMs.txt into your preferred AI system to ask questions, generate example code, or better understand how the API works.

Siri Shortcuts and Automation

Using the Timing API, you can quickly create Siri shortcuts to e.g. start and stop timers. During installation, the shortcuts will ask you for an API which you can generate in the 'API Keys' section of the web app. Once installed, simply run the corresponding shortcut to start or stop a timer.

The "Start Fixed Timing Timer" shortcut will ask you once for a combination of timer title and project name. From then on, it will always launch a shortcut with that combination of title and project. You can create several copies of this shortcut (e.g. by duplicating it), each with a different title/project combination.

Feel free to customize these shortcuts to your liking, e.g. by changing the "Start Timer" shortcut to let you select from a couple of preset titles instead. We are also interested in the shortcuts you create, so please let us know what you build with this!

You could also create scripts that start or stop a timer whenever you perform a specific action; see Start a new timer. and Stop the currently running timer. for the corresponding API calls.

Integrating with your billing system

The API also makes it possible to integrate with whatever billing system you are currently using. Simply retrieve your most recent time entries, then send them to your billing system in the desired format. You can also create a script to create a custom report in exactly the format you need, of course.

Make sure to have a look at the ?include_project_data=true query parameter to include the corresponding project's attributes in the response. This lets you retrieve project titles without having to do a second API call to the "Projects" collection.

The API also supports billing status tracking, which allows you to mark time entries as billable, not billable, billed, or paid. You can filter time entries by billing status and set billing status when creating or updating entries. For bulk operations, such as syncing back billing status updates from another system, you can use the batch update endpoint. For more details, see the billing status documentation in the time entries section.

GrandTotal integration

The GrandTotal plugin for Timing already uses the Timing Web API to import your team members' time entries. To use that functionality, please refer the corresponding section in the documentation.

Zapier integration

We also offer a Zapier integration. This lets you connect the API to thousands of other services with just a few clicks, solving the problems mentioned above without having to write any code. To start using this integration, see the Zapier section in the web app.

Example use cases include:

Contact us

We also recommend for you to reach out and let us know your desired use cases! This helps us prioritize which API features to build first.

API Usage

The API root is https://web.timingapp.com/api/v1. All endpoint URLs share this prefix. Sample code for each available API call can be found in the right-hand column. Note that query parameters need to be URL-encoded, as shown in the Bash example for the Return a list of time entries. call.

Authentication

The Timing API requires authentication with an API key. You can generate a key in the 'API Keys' section of the web app. Once generated, add an Authorization header to each request with value Bearer {{token}}, where {{token}} is your key.

Rate limiting

The API enforces a rate limit of 500 requests per hour. You can retrieve your current quota via the x-ratelimit-remaining header attached to every request. In addition, sending more than 200 requests per minute will also trigger a temporary rate limit.

Compression

All API requests should include an Accept-Encoding header. The API supports zstd (preferred) and gzip compression:

Accept-Encoding: zstd, gzip

Using zstd is strongly recommended as it provides significantly better compression ratios and faster decompression than gzip. Requests without an Accept-Encoding header will receive uncompressed responses, which wastes bandwidth and increases latency.

Request and response data

The API expects requests and returns responses in the JSON format. The actual response payload can be found in the data field. Additional data might be provided in the links and meta fields described below. Refer to the descriptions of individual API calls for concrete examples.

Repeated fields

Query parameters ending with a [] can be passed repeatedly. For example, passing ?columns[]=title&columns[]=notes will show both the "Title" and the "Notes" columns. Optionally, ascending indices can be provided (e.g. ?columns[0]=title&columns[1]=notes), which makes it easier to build queries in e.g. PHP.

Date format

As Timing is a time-tracking application, its API has to work with many dates. Dates returned by the API will always be formatted as an ISO8601 string including microseconds as well as the time zone, for example 2019-01-01T00:00:00.000000+00:00.

When sending dates in your requests, we recommend providing dates in a format appropriate to the type of request:

Timezone

When a timezone is not provided, the default timezone for an unqualified date is assumed to be UTC. For example, 2019-01-01T00:00:00 would be interpreted as 2019-01-01T00:00:00+0000. This may be subject to change, however, so try to provide a timezone with your request whenever possible.

A default timezone may be provided, using the X-Time-Zone header. When this header is set to a valid timezone, any unqualified date is assumed to be in this timezone instead of UTC.

The header only affects input parameters, and all dates are currently returned in UTC. This however may change in the future, so your code should not make any assumptions about this, and always account for the timezone specified in the results.

References

The Timing API identifies entities via the self field, which contains a link relative to the API root, for example /projects/1. This avoids any ambiguity about the type of the linked resource and provides you with a convenient way of looking it up, without having to look up the correct API call in this documentation: Simply append the link's value to the API root, resulting in e.g. https://web.timingapp.com/api/v1/projects/1.

References should be treated as opaque strings; your code should not assumptions about their structure.

Shallow references

For API responses that contain related entities, these entities are usually referenced in a "shallow" fashion. Instead of including the full object, a placeholder containing only the self field is provided, e.g. as "parent": {"self": "/projects/1"}. For the Return a list of time entries. call, you can append the ?include_project_data=true query parameter to include the corresponding project's attributes in the response. This lets you retrieve project titles without having to do a second API call to the "Projects" collection.

Some responses include links to related queries or entities. This includes pagination and queries for related entries.

Pagination

By default, collection responses are paginated to 100 items per page. The links to retrieve the first, last, next and previous pages are part of the response's links field. Additional information about the paginated data can be found in the meta field.

Custom fields

Some entities (namely projects and time entries) can have custom fields. These are returned as the custom_fields collection, with an entry for each custom field on the entity. Custom fields are intended only for scripting purposes. They are exposed only via the API and are not visible anywhere in the Timing app on your Mac nor the web app.

Naming

Custom field names must be a non-empty string, and may only contain alphanumeric characters, dashes and underscores. Additionally, they must not start with an underscore or contain only digits.

Values

Custom field values may only be strings. The only exception to this is providing null, which can be provided to remove a custom field.

Usage

Custom fields may be added when creating or updating an entity. When updating an entity, any custom fields not provided will be left untouched. If you want to remove a custom field, you must explicitly set it to null.

Activity Hierarchy

🧪 Beta — Build AI-powered time intelligence on top of Timing's activity data.

This endpoint provides a token-efficient, hierarchical view of user activities designed specifically for consumption by Large Language Models (LLMs) and AI agents. It returns plain text (not JSON) in a structured format that LLMs can parse reliably while minimizing token usage.

Use Cases

This endpoint enables AI agents to:

Design Philosophy

The Activity Hierarchy endpoint follows principles optimized for agentic workflows:

  1. Plain-text output: Returns tab-indented hierarchical text instead of JSON. This reduces token overhead by 30-50% compared to structured formats while remaining highly parseable by LLMs.

  2. Coarse-to-fine exploration: Start with high-level summaries (block_size=total), then drill down into specific time ranges only when needed. This prevents context window overflow.

  3. Composable with other endpoints: Combine with /projects/hierarchy to understand the user's project structure, then use activity data to suggest assignments or detect patterns.

  4. Adjustable detail level: The max_lines parameter lets you balance between comprehensive data and token efficiency based on your specific task.

Output Format

Activities are displayed hierarchically using tabs for indentation. The format uses intelligent compaction to minimize output size:

Example output:

Date range: 15.01.24, 00:00 – 15.01.24, 23:59
Total tracked: 8h 30m (30600 seconds)

6:00:00 Work ▸ Development
    2:15:00 Xcode | ~/Projects/MyApp
        1:30:00 Sources/App.swift
        0:45:00 Tests/AppTests.swift
    1:30:00 Safari | github.com
        0:45:00 /org/repo/pull/123 — Add new feature
        0:30:00 /org/repo/pull/124 — Fix bug
    0:45:00 Terminal | ~/Projects/MyApp | ✳ Build and Test
1:30:00 Work ▸ Communication
    1:00:00 Slack | Acme Inc.
    0:30:00 Mail | INBOX ▸ Work ▸ Notifications
1:00:00 (Unassigned)
    0:30:00 Safari | github.com/notifications
    0:30:00 Chrome | reddit.com/r/programming

Recommended Workflow

For summarizing activities:

  1. Call GET /projects/hierarchy once to understand the user's project structure
  2. Call this endpoint with block_size=total and default max_lines (100)
  3. Present findings to the user

For suggesting project assignments:

  1. Fetch all activities (no project_ids filter) to see assignment patterns
  2. Fetch unassigned activities with project_ids=0 and higher max_lines (200-500)
  3. Use patterns from step 1 to suggest projects for unassigned activities in step 2

Note: Assigning app usage to projects is not yet available via the API. Use project assignments to inform time entry creation, or present suggestions to the user for manual assignment in the app.

For identifying time entry boundaries:

  1. Start with block_size=hour to see the day's rhythm
  2. Drill into interesting hours with block_size=15min or 5min
  3. Use the transitions between activities to suggest time entry start/end times

Token Efficiency Guidelines

Task Recommended max_lines block_size
Quick summary 50 total
Day overview 100 (default) total or hour
Project categorization 200-500 total
Time entry creation 500 15min or 5min

For multi-day ranges, consider using block_size=day to get separate hierarchies per day rather than one massive combined view.

Getting Started

  1. Get your project structure: Call GET /api/v1/projects/hierarchy to retrieve your project IDs and understand how the user organizes their work
  2. Fetch activity overview: Call this endpoint with block_size=total for the big picture
  3. Drill down as needed: Use finer block_size values or filter by project_ids

Special Values

Timezones

Get activity hierarchy.

requires authentication

Returns a plain-text hierarchical view of user activities grouped by project and application. Use this as your primary data source when building AI-powered time intelligence features.

Strategy: Coarse to Fine

Avoid fetching more data than needed. Start broad, then drill down:

  1. Overview first: Use block_size=total to get the big picture
  2. Chronological view: Use block_size=hour or day to see when activities happened
  3. Precise boundaries: Only use 15min or 5min when identifying exact transition points

Suggesting Project Assignments

Many activities are already assigned to projects via automatic rules. To suggest assignments for unassigned activities:

  1. Fetch ALL activities (omit project_ids) to learn existing assignment patterns
  2. Fetch unassigned activities with project_ids=0 and higher max_lines
  3. Match unassigned activities against patterns you observed in assigned activities

Understanding the Output

The output uses tab-based indentation to show hierarchy:

Practical Examples

"Summarize my day": ?start_date=2024-01-15&end_date=2024-01-15&block_size=total&max_lines=50

"What did I work on this week?": ?start_date=2024-01-08&end_date=2024-01-14&block_size=day&max_lines=100

"Show unassigned time yesterday": ?start_date=2024-01-14&end_date=2024-01-14&project_ids=0&max_lines=200

"When did I switch between tasks?": ?start_date=2024-01-15&end_date=2024-01-15&block_size=hour&max_lines=200

Example request:
curl --request GET \
    --get "https://web.timingapp.com/api/v1/activity-hierarchy?start_date=2024-01-01&end_date=2024-01-31&block_size=total&minimum_duration_seconds=60&group_by_project=true&max_depth=0&max_lines=100&include_mobile_devices=false&include_subprojects=true" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/activity-hierarchy"
);

const params = {
    "start_date": "2024-01-01",
    "end_date": "2024-01-31",
    "block_size": "total",
    "minimum_duration_seconds": "60",
    "group_by_project": "true",
    "max_depth": "0",
    "max_lines": "100",
    "include_mobile_devices": "false",
    "include_subprojects": "true",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/activity-hierarchy';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'query' => [
            'start_date' => '2024-01-01',
            'end_date' => '2024-01-31',
            'block_size' => 'total',
            'minimum_duration_seconds' => '60',
            'group_by_project' => 'true',
            'max_depth' => '0',
            'max_lines' => '100',
            'include_mobile_devices' => 'false',
            'include_subprojects' => 'true',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/activity-hierarchy'
params = {
  'start_date': '2024-01-01',
  'end_date': '2024-01-31',
  'block_size': 'total',
  'minimum_duration_seconds': '60',
  'group_by_project': 'true',
  'max_depth': '0',
  'max_lines': '100',
  'include_mobile_devices': 'false',
  'include_subprojects': 'true',
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200, Activity hierarchy as plain text):

Date range: 15.01.24, 00:00 – 15.01.24, 23:59
Total tracked: 8h 30m (30600 seconds)

6:00:00 Work ▸ Development
	2:15:00 Xcode | ~/Projects/MyApp
		1:30:00 Sources/App.swift
		0:45:00 Tests/AppTests.swift
	1:30:00 Safari | github.com
		0:45:00 /org/repo/pull/123 — Add new feature
		0:30:00 /org/repo/pull/124 — Fix bug in auth
		0:15:00 /org/repo/issues
	1:00:00 Chrome | docs.example.com/api
		0:40:00 Authentication Guide
		0:20:00 Rate Limiting
	0:45:00 Safari | stackoverflow.com/questions/12345 — How to fix...
	0:30:00 Terminal | ~/Projects/MyApp | ✳ Build and Test
1:30:00 Work ▸ Communication
	1:00:00 Slack | Acme Inc.
		0:40:00 general (Channel)
		0:20:00 code-review (Channel)
	0:30:00 Mail | INBOX ▸ Work ▸ Notifications
1:00:00 (Unassigned)
	0:30:00 Safari | github.com/notifications
	0:30:00 Chrome | reddit.com/r/programming
		0:20:00 New post about Swift
		0:10:00 Discussion thread

 

Projects

List projects hierarchically.

requires authentication


Return the complete project hierarchy.

See Display the specified project. for the returned attributes.

Example request:
curl --request GET \
    --get "https://web.timingapp.com/api/v1/projects/hierarchy" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/projects/hierarchy"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/projects/hierarchy';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/projects/hierarchy'
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": [
        {
            "self": "/projects/1",
            "team_id": null,
            "title": "Project at root level",
            "title_chain": [
                "Project at root level"
            ],
            "color": "#FF0000",
            "productivity_score": 1,
            "is_archived": false,
            "notes": null,
            "children": [
                {
                    "self": "/projects/2",
                    "team_id": null,
                    "title": "Unproductive child project",
                    "title_chain": [
                        "Project at root level",
                        "Unproductive child project"
                    ],
                    "color": "#00FF00",
                    "productivity_score": -1,
                    "is_archived": false,
                    "notes": null,
                    "children": [],
                    "parent": {
                        "self": "/projects/1"
                    },
                    "default_billing_status": "not_billable",
                    "custom_fields": {}
                }
            ],
            "parent": null,
            "default_billing_status": "billable",
            "custom_fields": {}
        }
    ]
}
 

List projects.

requires authentication


Return a list containing all projects.

See Display the specified project. for the returned attributes.

Example request:
curl --request GET \
    --get "https://web.timingapp.com/api/v1/projects?title=root&hide_archived=1" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/projects"
);

const params = {
    "title": "root",
    "hide_archived": "1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/projects';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'query' => [
            'title' => 'root',
            'hide_archived' => '1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/projects'
params = {
  'title': 'root',
  'hide_archived': '1',
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": [
        {
            "self": "/projects/1",
            "team_id": null,
            "title": "Project at root level",
            "title_chain": [
                "Project at root level"
            ],
            "color": "#FF0000",
            "productivity_score": 1,
            "is_archived": false,
            "notes": null,
            "children": [
                {
                    "self": "/projects/2"
                }
            ],
            "parent": null,
            "default_billing_status": "billable",
            "custom_fields": {}
        }
    ]
}
 

Create project.

requires authentication


Create a new project.

See Display the specified project. for the returned attributes.

Example request:
curl --request POST \
    "https://web.timingapp.com/api/v1/projects" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip" \
    --data "{
    \"title\": \"Acme Inc.\",
    \"parent\": \"\\/projects\\/1\",
    \"color\": \"#FF0000\",
    \"productivity_score\": 1,
    \"is_archived\": false,
    \"notes\": \"Some more detailed notes\",
    \"default_billing_status\": \"billable\",
    \"custom_fields\": {
        \"field_name\": \"field_value\"
    }
}"
const url = new URL(
    "https://web.timingapp.com/api/v1/projects"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};

let body = {
    "title": "Acme Inc.",
    "parent": "\/projects\/1",
    "color": "#FF0000",
    "productivity_score": 1,
    "is_archived": false,
    "notes": "Some more detailed notes",
    "default_billing_status": "billable",
    "custom_fields": {
        "field_name": "field_value"
    }
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/projects';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'json' => [
            'title' => 'Acme Inc.',
            'parent' => '/projects/1',
            'color' => '#FF0000',
            'productivity_score' => 1.0,
            'is_archived' => false,
            'notes' => 'Some more detailed notes',
            'default_billing_status' => 'billable',
            'custom_fields' => [
                'field_name' => 'field_value',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/projects'
payload = {
    "title": "Acme Inc.",
    "parent": "\/projects\/1",
    "color": "#FF0000",
    "productivity_score": 1,
    "is_archived": false,
    "notes": "Some more detailed notes",
    "default_billing_status": "billable",
    "custom_fields": {
        "field_name": "field_value"
    }
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (201):

Show headers
content-type: application/json
 
{
    "data": {
        "self": "/projects/2",
        "team_id": null,
        "title": "Acme Inc.",
        "title_chain": [
            "Project at root level",
            "Acme Inc."
        ],
        "color": "#FF0000",
        "productivity_score": 1,
        "is_archived": false,
        "notes": "Some more detailed notes",
        "children": [],
        "parent": {
            "self": "/projects/1"
        },
        "default_billing_status": "billable",
        "custom_fields": {
            "field_name": "field_value"
        }
    },
    "links": {
        "time-entries": "https://web.timingapp.com/api/v1/time-entries?project[]=/projects/2"
    }
}
 

Show project.

requires authentication


Display the specified project.

The following attributes will be returned:

Example request:
curl --request GET \
    --get "https://web.timingapp.com/api/v1/projects/1" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/projects/1"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/projects/1';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/projects/1'
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": {
        "self": "/projects/1",
        "team_id": null,
        "title": "Project at root level",
        "title_chain": [
            "Project at root level"
        ],
        "color": "#FF0000",
        "productivity_score": 1,
        "is_archived": false,
        "notes": null,
        "children": [
            {
                "self": "/projects/2"
            }
        ],
        "parent": null,
        "default_billing_status": "billable",
        "custom_fields": {}
    },
    "links": {
        "time-entries": "https://web.timingapp.com/api/v1/time-entries?project[]=/projects/1"
    }
}
 

Update project.

requires authentication


Update the specified project.

See Display the specified project. for the returned attributes.

Example request:
curl --request PUT \
    "https://web.timingapp.com/api/v1/projects/1" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip" \
    --data "{
    \"title\": \"Acme Inc.\",
    \"color\": \"#FF0000\",
    \"productivity_score\": 1,
    \"is_archived\": false,
    \"notes\": \"Some more detailed notes\",
    \"default_billing_status\": \"billable\",
    \"custom_fields\": {
        \"field_name\": \"field_value\"
    }
}"
const url = new URL(
    "https://web.timingapp.com/api/v1/projects/1"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};

let body = {
    "title": "Acme Inc.",
    "color": "#FF0000",
    "productivity_score": 1,
    "is_archived": false,
    "notes": "Some more detailed notes",
    "default_billing_status": "billable",
    "custom_fields": {
        "field_name": "field_value"
    }
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/projects/1';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'json' => [
            'title' => 'Acme Inc.',
            'color' => '#FF0000',
            'productivity_score' => 1.0,
            'is_archived' => false,
            'notes' => 'Some more detailed notes',
            'default_billing_status' => 'billable',
            'custom_fields' => [
                'field_name' => 'field_value',
            ],
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/projects/1'
payload = {
    "title": "Acme Inc.",
    "color": "#FF0000",
    "productivity_score": 1,
    "is_archived": false,
    "notes": "Some more detailed notes",
    "default_billing_status": "billable",
    "custom_fields": {
        "field_name": "field_value"
    }
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('PUT', url, headers=headers, json=payload)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": {
        "self": "/projects/1",
        "team_id": null,
        "title": "Acme Inc.",
        "title_chain": [
            "Acme Inc."
        ],
        "color": "#FF0000",
        "productivity_score": 1,
        "is_archived": false,
        "notes": "Some more detailed notes",
        "children": [
            {
                "self": "/projects/2"
            }
        ],
        "parent": null,
        "default_billing_status": "billable",
        "custom_fields": {
            "field_name": "field_value"
        }
    },
    "links": {
        "time-entries": "https://web.timingapp.com/api/v1/time-entries?project[]=/projects/1"
    }
}
 

Delete project.

requires authentication


Delete the specified project and all of its children.

Example request:
curl --request DELETE \
    "https://web.timingapp.com/api/v1/projects/1" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/projects/1"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/projects/1';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/projects/1'
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('DELETE', url, headers=headers)
response.json()

Example response (204):

Empty response
 

Reports

Generate report.

requires authentication


Generate a report that can contain both time entries and app usage.

Returns a JSON array with several rows; each row includes the total duration (in seconds) belonging to the corresponding other (configurable) columns.

The include_app_usage and include_team_members parameters govern whether to include app usage (otherwise, only time entries are returned) as well as data for other team members.
The start_date_min, start_date_max, projects(also see include_child_projects) and search_query parameters allow filtering the returned data.
The columns, project_grouping_level, include_project_data, timespan_grouping_mode, and sort parameters govern the presentation of the returned data.

Example request:
curl --request GET \
    --get "https://web.timingapp.com/api/v1/report?include_app_usage=0&include_team_members=0&team_members[]=%2Fusers%2F1&start_date_min=2019-01-01&start_date_max=2019-01-01&projects[]=%2Fprojects%2F1&include_child_projects=1&search_query=meeting&billing_status[]=billable&columns[]=project&project_grouping_level=0&include_project_data=1&timespan_grouping_mode=day&sort[]=-duration" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/report"
);

const params = {
    "include_app_usage": "0",
    "include_team_members": "0",
    "team_members[0]": "/users/1",
    "start_date_min": "2019-01-01",
    "start_date_max": "2019-01-01",
    "projects[0]": "/projects/1",
    "include_child_projects": "1",
    "search_query": "meeting",
    "billing_status[0]": "billable",
    "columns[0]": "project",
    "project_grouping_level": "0",
    "include_project_data": "1",
    "timespan_grouping_mode": "day",
    "sort[0]": "-duration",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/report';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'query' => [
            'include_app_usage' => '0',
            'include_team_members' => '0',
            'team_members[0]' => '/users/1',
            'start_date_min' => '2019-01-01',
            'start_date_max' => '2019-01-01',
            'projects[0]' => '/projects/1',
            'include_child_projects' => '1',
            'search_query' => 'meeting',
            'billing_status[0]' => 'billable',
            'columns[0]' => 'project',
            'project_grouping_level' => '0',
            'include_project_data' => '1',
            'timespan_grouping_mode' => 'day',
            'sort[0]' => '-duration',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/report'
params = {
  'include_app_usage': '0',
  'include_team_members': '0',
  'team_members[0]': '/users/1',
  'start_date_min': '2019-01-01',
  'start_date_max': '2019-01-01',
  'projects[0]': '/projects/1',
  'include_child_projects': '1',
  'search_query': 'meeting',
  'billing_status[0]': 'billable',
  'columns[0]': 'project',
  'project_grouping_level': '0',
  'include_project_data': '1',
  'timespan_grouping_mode': 'day',
  'sort[0]': '-duration',
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": [
        {
            "duration": 3600,
            "project": {
                "self": "/projects/1",
                "team_id": null,
                "title": "Project at root level",
                "title_chain": [
                    "Project at root level"
                ],
                "color": "#FF0000",
                "productivity_score": 1,
                "is_archived": false,
                "notes": null,
                "parent": null,
                "default_billing_status": "billable",
                "custom_fields": {}
            }
        }
    ]
}
 

Teams

List team members.

requires authentication


Return a list containing all active members of the given team.

Members with pending invitations will be excluded.

The following attributes will be returned:

Example request:
curl --request GET \
    --get "https://web.timingapp.com/api/v1/teams/1/members" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/teams/1/members"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/teams/1/members';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/teams/1/members'
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": [
        {
            "self": "/users/1",
            "email": "johnny@appleseed.net",
            "name": "Johnny Appleseed"
        }
    ]
}
 

List teams.

requires authentication


Return a list containing all the teams you are a member of.

Example request:
curl --request GET \
    --get "https://web.timingapp.com/api/v1/teams" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/teams"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/teams';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/teams'
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": [
        {
            "id": "/teams/1",
            "name": "Demo Team"
        }
    ]
}
 

Time Entries

Start timer.

requires authentication


Start a new timer.

This also stops the currently running timer if there is one.

See Display the specified time entry. for the returned attributes.

Example request:
curl --request POST \
    "https://web.timingapp.com/api/v1/time-entries/start" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip" \
    --data "{
    \"start_date\": \"2019-01-01T00:00:00+00:00\",
    \"project\": \"Unproductive child project\",
    \"title\": \"Client Meeting\",
    \"notes\": \"Some more detailed notes\",
    \"replace_existing\": false,
    \"custom_fields\": {
        \"field_name\": \"field_value\"
    },
    \"billing_status\": \"billable\"
}"
const url = new URL(
    "https://web.timingapp.com/api/v1/time-entries/start"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};

let body = {
    "start_date": "2019-01-01T00:00:00+00:00",
    "project": "Unproductive child project",
    "title": "Client Meeting",
    "notes": "Some more detailed notes",
    "replace_existing": false,
    "custom_fields": {
        "field_name": "field_value"
    },
    "billing_status": "billable"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/time-entries/start';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'json' => [
            'start_date' => '2019-01-01T00:00:00+00:00',
            'project' => 'Unproductive child project',
            'title' => 'Client Meeting',
            'notes' => 'Some more detailed notes',
            'replace_existing' => false,
            'custom_fields' => [
                'field_name' => 'field_value',
            ],
            'billing_status' => 'billable',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/time-entries/start'
payload = {
    "start_date": "2019-01-01T00:00:00+00:00",
    "project": "Unproductive child project",
    "title": "Client Meeting",
    "notes": "Some more detailed notes",
    "replace_existing": false,
    "custom_fields": {
        "field_name": "field_value"
    },
    "billing_status": "billable"
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (201):

Show headers
content-type: application/json
 
{
    "data": {
        "self": "/time-entries/2",
        "start_date": "2019-01-01T00:00:00.000000+00:00",
        "end_date": "2019-01-01T00:00:00.000000+00:00",
        "duration": 0,
        "project": {
            "self": "/projects/2"
        },
        "title": "Client Meeting",
        "notes": "Some more detailed notes",
        "is_running": true,
        "creator_id": "/users/1",
        "creator_name": "Johnny Appleseed",
        "billing_status": "billable",
        "custom_fields": {
            "field_name": "field_value"
        }
    },
    "message": "Timer 'Client Meeting' started."
}
 

Stop timer.

requires authentication


Stop the currently running timer.

Returns the stopped timer's attributes as listed under Display the specified time entry..

Example request:
curl --request PUT \
    "https://web.timingapp.com/api/v1/time-entries/stop" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/time-entries/stop"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "PUT",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/time-entries/stop';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/time-entries/stop'
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('PUT', url, headers=headers)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": {
        "self": "/time-entries/1",
        "start_date": "2019-01-01T00:00:00.000000+00:00",
        "end_date": "2019-01-01T01:00:00.000000+00:00",
        "duration": 3600,
        "project": {
            "self": "/projects/1"
        },
        "title": "Client Meeting",
        "notes": "Some more detailed notes",
        "is_running": false,
        "creator_id": "/users/1",
        "creator_name": "Johnny Appleseed",
        "billing_status": "billable",
        "custom_fields": {}
    },
    "message": "Timer 'Client Meeting' stopped."
}
 

Show latest time entry.

requires authentication


Redirect to the latest time entry.

See Display the specified time entry. for the route the redirect points to.

Example request:
curl --request GET \
    --get "https://web.timingapp.com/api/v1/time-entries/latest" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/time-entries/latest"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/time-entries/latest';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/time-entries/latest'
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (302):

Show headers
location: https://web.timingapp.com/api/v1/time-entries/1
content-type: text/html; charset=utf-8
 
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='https://web.timingapp.com/api/v1/time-entries/1'" />

        <title>Redirecting to https://web.timingapp.com/api/v1/time-entries/1</title>
    </head>
    <body>
        Redirecting to <a href="https://web.timingapp.com/api/v1/time-entries/1">https://web.timingapp.com/api/v1/time-entries/1</a>.
    </body>
</html>
 

Show running timer.


Redirect to the currently running timer.

See Display the specified time entry. for the route the redirect points to.

Example request:
curl --request GET \
    --get "https://web.timingapp.com/api/v1/time-entries/running" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/time-entries/running"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/time-entries/running';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/time-entries/running'
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('GET', url, headers=headers)
response.json()

Example response (404):

Show headers
content-type: application/json
 
{
    "message": "No running timer found."
}
 

Batch update time entries.

requires authentication


Update multiple time entries at once.

This endpoint allows updating multiple time entries with the same data. All provided time entries will be updated with the fields specified in the data object.

Example request:
curl --request PATCH \
    "https://web.timingapp.com/api/v1/time-entries/batch-update" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip" \
    --data "{
    \"time_entries\": [
        1,
        2,
        3
    ],
    \"data\": {
        \"billing_status\": \"billed\",
        \"project\": \"Unproductive child project\",
        \"title\": \"Client Meeting\",
        \"notes\": \"Some more detailed notes\",
        \"custom_fields\": {
            \"field_name\": \"field_value\"
        }
    },
    \"allow_editing_other_users\": false
}"
const url = new URL(
    "https://web.timingapp.com/api/v1/time-entries/batch-update"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};

let body = {
    "time_entries": [
        1,
        2,
        3
    ],
    "data": {
        "billing_status": "billed",
        "project": "Unproductive child project",
        "title": "Client Meeting",
        "notes": "Some more detailed notes",
        "custom_fields": {
            "field_name": "field_value"
        }
    },
    "allow_editing_other_users": false
};

fetch(url, {
    method: "PATCH",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/time-entries/batch-update';
$response = $client->patch(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'json' => [
            'time_entries' => [
                1,
                2,
                3,
            ],
            'data' => [
                'billing_status' => 'billed',
                'project' => 'Unproductive child project',
                'title' => 'Client Meeting',
                'notes' => 'Some more detailed notes',
                'custom_fields' => [
                    'field_name' => 'field_value',
                ],
            ],
            'allow_editing_other_users' => false,
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/time-entries/batch-update'
payload = {
    "time_entries": [
        1,
        2,
        3
    ],
    "data": {
        "billing_status": "billed",
        "project": "Unproductive child project",
        "title": "Client Meeting",
        "notes": "Some more detailed notes",
        "custom_fields": {
            "field_name": "field_value"
        }
    },
    "allow_editing_other_users": false
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('PATCH', url, headers=headers, json=payload)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": [
        {
            "self": "/time-entries/1",
            "start_date": "2019-01-01T00:00:00.000000+00:00",
            "end_date": "2019-01-01T01:00:00.000000+00:00",
            "duration": 3600,
            "project": {
                "self": "/projects/2"
            },
            "title": "Client Meeting",
            "notes": "Some more detailed notes",
            "is_running": false,
            "creator_id": "/users/1",
            "creator_name": "Johnny Appleseed",
            "billing_status": "billed",
            "custom_fields": {
                "field_name": "field_value"
            }
        },
        {
            "self": "/time-entries/2",
            "start_date": "2019-01-01T02:00:00.000000+00:00",
            "end_date": "2019-01-01T03:00:00.000000+00:00",
            "duration": 3600,
            "project": {
                "self": "/projects/2"
            },
            "title": "Client Meeting",
            "notes": "Some more detailed notes",
            "is_running": false,
            "creator_id": "/users/1",
            "creator_name": "Johnny Appleseed",
            "billing_status": "billed",
            "custom_fields": {
                "field_name": "field_value"
            }
        },
        {
            "self": "/time-entries/3",
            "start_date": "2019-01-01T04:00:00.000000+00:00",
            "end_date": "2019-01-01T05:00:00.000000+00:00",
            "duration": 3600,
            "project": {
                "self": "/projects/2"
            },
            "title": "Client Meeting",
            "notes": "Some more detailed notes",
            "is_running": false,
            "creator_id": "/users/1",
            "creator_name": "Johnny Appleseed",
            "billing_status": "billed",
            "custom_fields": {
                "field_name": "field_value"
            }
        }
    ],
    "message": "Updated 3 time entries."
}
 

List time entries.

requires authentication


Return a list of time entries.

See Display the specified time entry. for the returned attributes.

Items are ordered descending by their start_date field.

Example request:
curl --request GET \
    --get "https://web.timingapp.com/api/v1/time-entries?start_date_min=2019-01-01&start_date_max=2019-01-01&projects[]=%2Fprojects%2F1&include_child_projects=1&search_query=meeting&is_running=0&include_project_data=1&include_team_members=0&team_members[]=%2Fusers%2F1&billing_status[]=billable" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/time-entries"
);

const params = {
    "start_date_min": "2019-01-01",
    "start_date_max": "2019-01-01",
    "projects[0]": "/projects/1",
    "include_child_projects": "1",
    "search_query": "meeting",
    "is_running": "0",
    "include_project_data": "1",
    "include_team_members": "0",
    "team_members[0]": "/users/1",
    "billing_status[0]": "billable",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/time-entries';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'query' => [
            'start_date_min' => '2019-01-01',
            'start_date_max' => '2019-01-01',
            'projects[0]' => '/projects/1',
            'include_child_projects' => '1',
            'search_query' => 'meeting',
            'is_running' => '0',
            'include_project_data' => '1',
            'include_team_members' => '0',
            'team_members[0]' => '/users/1',
            'billing_status[0]' => 'billable',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/time-entries'
params = {
  'start_date_min': '2019-01-01',
  'start_date_max': '2019-01-01',
  'projects[0]': '/projects/1',
  'include_child_projects': '1',
  'search_query': 'meeting',
  'is_running': '0',
  'include_project_data': '1',
  'include_team_members': '0',
  'team_members[0]': '/users/1',
  'billing_status[0]': 'billable',
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": [
        {
            "self": "/time-entries/1",
            "start_date": "2019-01-01T00:00:00.000000+00:00",
            "end_date": "2019-01-01T01:00:00.000000+00:00",
            "duration": 3600,
            "project": {
                "self": "/projects/1",
                "team_id": null,
                "title": "Project at root level",
                "title_chain": [
                    "Project at root level"
                ],
                "color": "#FF0000",
                "productivity_score": 1,
                "is_archived": false,
                "notes": null,
                "parent": null,
                "default_billing_status": "billable",
                "custom_fields": {}
            },
            "title": "Client Meeting",
            "notes": "Some more detailed notes",
            "is_running": false,
            "creator_id": "/users/1",
            "creator_name": "Johnny Appleseed",
            "billing_status": "billable",
            "custom_fields": {}
        }
    ],
    "links": {
        "first": "http://timing-web.test/api/v1/time-entries?start_date_min=2019-01-01&start_date_max=2019-01-01&projects%5B0%5D=%2Fprojects%2F1&include_child_projects=1&search_query=meeting&is_running=0&include_project_data=1&include_team_members=0&team_members%5B0%5D=%2Fusers%2F1&billing_status%5B0%5D=billable&page=1",
        "last": "http://timing-web.test/api/v1/time-entries?start_date_min=2019-01-01&start_date_max=2019-01-01&projects%5B0%5D=%2Fprojects%2F1&include_child_projects=1&search_query=meeting&is_running=0&include_project_data=1&include_team_members=0&team_members%5B0%5D=%2Fusers%2F1&billing_status%5B0%5D=billable&page=1",
        "prev": null,
        "next": null
    },
    "meta": {
        "current_page": 1,
        "from": 1,
        "last_page": 1,
        "links": [
            {
                "url": null,
                "label": "&laquo; Previous",
                "page": null,
                "active": false
            },
            {
                "url": "http://timing-web.test/api/v1/time-entries?start_date_min=2019-01-01&start_date_max=2019-01-01&projects%5B0%5D=%2Fprojects%2F1&include_child_projects=1&search_query=meeting&is_running=0&include_project_data=1&include_team_members=0&team_members%5B0%5D=%2Fusers%2F1&billing_status%5B0%5D=billable&page=1",
                "label": "1",
                "page": 1,
                "active": true
            },
            {
                "url": null,
                "label": "Next &raquo;",
                "page": null,
                "active": false
            }
        ],
        "path": "http://timing-web.test/api/v1/time-entries",
        "per_page": 1000,
        "to": 1,
        "total": 1
    }
}
 

Create time entry.

requires authentication


Create a new time entry.

See Display the specified time entry. for the returned attributes.

Example request:
curl --request POST \
    "https://web.timingapp.com/api/v1/time-entries" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip" \
    --data "{
    \"start_date\": \"2019-01-01T00:00:00+00:00\",
    \"end_date\": \"2019-01-01T01:00:00+00:00\",
    \"project\": \"Unproductive child project\",
    \"title\": \"Client Meeting\",
    \"notes\": \"Some more detailed notes\",
    \"replace_existing\": false,
    \"custom_fields\": {
        \"field_name\": \"field_value\"
    },
    \"billing_status\": \"billable\"
}"
const url = new URL(
    "https://web.timingapp.com/api/v1/time-entries"
);

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};

let body = {
    "start_date": "2019-01-01T00:00:00+00:00",
    "end_date": "2019-01-01T01:00:00+00:00",
    "project": "Unproductive child project",
    "title": "Client Meeting",
    "notes": "Some more detailed notes",
    "replace_existing": false,
    "custom_fields": {
        "field_name": "field_value"
    },
    "billing_status": "billable"
};

fetch(url, {
    method: "POST",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/time-entries';
$response = $client->post(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'json' => [
            'start_date' => '2019-01-01T00:00:00+00:00',
            'end_date' => '2019-01-01T01:00:00+00:00',
            'project' => 'Unproductive child project',
            'title' => 'Client Meeting',
            'notes' => 'Some more detailed notes',
            'replace_existing' => false,
            'custom_fields' => [
                'field_name' => 'field_value',
            ],
            'billing_status' => 'billable',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/time-entries'
payload = {
    "start_date": "2019-01-01T00:00:00+00:00",
    "end_date": "2019-01-01T01:00:00+00:00",
    "project": "Unproductive child project",
    "title": "Client Meeting",
    "notes": "Some more detailed notes",
    "replace_existing": false,
    "custom_fields": {
        "field_name": "field_value"
    },
    "billing_status": "billable"
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('POST', url, headers=headers, json=payload)
response.json()

Example response (201):

Show headers
content-type: application/json
 
{
    "data": {
        "self": "/time-entries/2",
        "start_date": "2019-01-01T00:00:00.000000+00:00",
        "end_date": "2019-01-01T01:00:00.000000+00:00",
        "duration": 3600,
        "project": {
            "self": "/projects/2"
        },
        "title": "Client Meeting",
        "notes": "Some more detailed notes",
        "is_running": false,
        "creator_id": "/users/1",
        "creator_name": "Johnny Appleseed",
        "billing_status": "billable",
        "custom_fields": {
            "field_name": "field_value"
        }
    }
}
 

Show time entry.

requires authentication


Display the specified time entry.

The following attributes will be returned:

Example request:
curl --request GET \
    --get "https://web.timingapp.com/api/v1/time-entries/1?other_user_id=%2Fusers%2F1" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/time-entries/1"
);

const params = {
    "other_user_id": "/users/1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "GET",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/time-entries/1';
$response = $client->get(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'query' => [
            'other_user_id' => '/users/1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/time-entries/1'
params = {
  'other_user_id': '/users/1',
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('GET', url, headers=headers, params=params)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": {
        "self": "/time-entries/1",
        "start_date": "2019-01-01T00:00:00.000000+00:00",
        "end_date": "2019-01-01T01:00:00.000000+00:00",
        "duration": 3600,
        "project": {
            "self": "/projects/1"
        },
        "title": "Client Meeting",
        "notes": "Some more detailed notes",
        "is_running": false,
        "creator_id": "/users/1",
        "creator_name": "Johnny Appleseed",
        "billing_status": "billable",
        "custom_fields": {}
    }
}
 

Update time entry.

requires authentication


Update the specified time entry.

See Display the specified time entry. for the returned attributes.

Example request:
curl --request PUT \
    "https://web.timingapp.com/api/v1/time-entries/1?other_user_id=%2Fusers%2F1" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip" \
    --data "{
    \"start_date\": \"2019-01-01T00:00:00+00:00\",
    \"end_date\": \"2019-01-01T01:00:00+00:00\",
    \"project\": \"Unproductive child project\",
    \"title\": \"Client Meeting\",
    \"notes\": \"Some more detailed notes\",
    \"replace_existing\": false,
    \"custom_fields\": {
        \"field_name\": \"field_value\"
    },
    \"billing_status\": \"billable\"
}"
const url = new URL(
    "https://web.timingapp.com/api/v1/time-entries/1"
);

const params = {
    "other_user_id": "/users/1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};

let body = {
    "start_date": "2019-01-01T00:00:00+00:00",
    "end_date": "2019-01-01T01:00:00+00:00",
    "project": "Unproductive child project",
    "title": "Client Meeting",
    "notes": "Some more detailed notes",
    "replace_existing": false,
    "custom_fields": {
        "field_name": "field_value"
    },
    "billing_status": "billable"
};

fetch(url, {
    method: "PUT",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/time-entries/1';
$response = $client->put(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'query' => [
            'other_user_id' => '/users/1',
        ],
        'json' => [
            'start_date' => '2019-01-01T00:00:00+00:00',
            'end_date' => '2019-01-01T01:00:00+00:00',
            'project' => 'Unproductive child project',
            'title' => 'Client Meeting',
            'notes' => 'Some more detailed notes',
            'replace_existing' => false,
            'custom_fields' => [
                'field_name' => 'field_value',
            ],
            'billing_status' => 'billable',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/time-entries/1'
payload = {
    "start_date": "2019-01-01T00:00:00+00:00",
    "end_date": "2019-01-01T01:00:00+00:00",
    "project": "Unproductive child project",
    "title": "Client Meeting",
    "notes": "Some more detailed notes",
    "replace_existing": false,
    "custom_fields": {
        "field_name": "field_value"
    },
    "billing_status": "billable"
}
params = {
  'other_user_id': '/users/1',
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('PUT', url, headers=headers, json=payload, params=params)
response.json()

Example response (200):

Show headers
content-type: application/json
 
{
    "data": {
        "self": "/time-entries/1",
        "start_date": "2019-01-01T00:00:00.000000+00:00",
        "end_date": "2019-01-01T01:00:00.000000+00:00",
        "duration": 3600,
        "project": {
            "self": "/projects/2"
        },
        "title": "Client Meeting",
        "notes": "Some more detailed notes",
        "is_running": false,
        "creator_id": "/users/1",
        "creator_name": "Johnny Appleseed",
        "billing_status": "billable",
        "custom_fields": {
            "field_name": "field_value"
        }
    }
}
 

Delete time entry.

requires authentication


Delete the specified time entry.

Example request:
curl --request DELETE \
    "https://web.timingapp.com/api/v1/time-entries/1?other_user_id=%2Fusers%2F1" \
    --header "Authorization: Bearer {{token}}" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --header "Accept-Encoding: zstd, gzip"
const url = new URL(
    "https://web.timingapp.com/api/v1/time-entries/1"
);

const params = {
    "other_user_id": "/users/1",
};
Object.keys(params)
    .forEach(key => url.searchParams.append(key, params[key]));

const headers = {
    "Authorization": "Bearer {{token}}",
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Accept-Encoding": "zstd, gzip",
};


fetch(url, {
    method: "DELETE",
    headers,
}).then(response => response.json());
$client = new \GuzzleHttp\Client();
$url = 'https://web.timingapp.com/api/v1/time-entries/1';
$response = $client->delete(
    $url,
    [
        'headers' => [
            'Authorization' => 'Bearer {{token}}',
            'Content-Type' => 'application/json',
            'Accept' => 'application/json',
            'Accept-Encoding' => 'zstd, gzip',
        ],
        'query' => [
            'other_user_id' => '/users/1',
        ],
    ]
);
$body = $response->getBody();
print_r(json_decode((string) $body));
import requests
import json

url = 'https://web.timingapp.com/api/v1/time-entries/1'
params = {
  'other_user_id': '/users/1',
}
headers = {
  'Authorization': 'Bearer {{token}}',
  'Content-Type': 'application/json',
  'Accept': 'application/json',
  'Accept-Encoding': 'zstd, gzip'
}

response = requests.request('DELETE', url, headers=headers, params=params)
response.json()

Example response (204):

Empty response