Skip to content

Instantly share code, notes, and snippets.

@tscheepers
Last active April 5, 2022 12:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tscheepers/f7b6a7c74d60dfbafdbd004ef9aef5bd to your computer and use it in GitHub Desktop.
Save tscheepers/f7b6a7c74d60dfbafdbd004ef9aef5bd to your computer and use it in GitHub Desktop.
{
"openapi": "3.0.0",
"info": {
"title": "Keeping API Documentation",
"version": "v1",
"x-logo": {
"url": "https://developer.keeping.nl/images/logo.svg",
"altText": "Keeping logo"
},
"description": "# Introduction\nReady to create an integration with [Keeping](https://keeping.nl)? Developers can use our [RESTful](https://en.m.wikipedia.org/wiki/Representational_state_transfer) API to manipulate time entries programmatically, fetch reports, manage users, projects, tasks and clients. Use the API by communicating with the accompanying web service over [HTTPS](https://en.wikipedia.org/wiki/HTTPS) at `api.keeping.nl/v1`. The [JSON](https://en.wikipedia.org/wiki/JSON) format is used for both request and response bodies.\n\n## Quick start \n\nTo obtain quick access to the web service, and start using the API, navigate to [**Developer**](https://keeping.nl/account/developer) under **My account** within Keeping. There you can either generate a personal access token to try the API yourself, or configure an OAuth2 client for others to use your integration. For a quick start, choose the personal access token. \n\nOnce you have generated a personal access token, use it to send a `GET` request to: [`https://api.keeping.nl/v1/organisations`](#tag/organisations/paths/~1organisations/get) with an `Authorization` header containing `Bearer {your personal access token}`. For example, you could use [cURL](https://en.wikipedia.org/wiki/CURL) in the command line as follows:\n\n```bash\ncurl -XGET https://api.keeping.nl/v1/organisations \\\n -H \"Authorization: Bearer {your access token}\" \\\n -H \"Accept: application/json\"\n```\n\nThis command will return a JSON object containing all the organisations you have access to. Select one to use and send subsequent requests to: [`https://api.keeping.nl/v1/{organisation_id}/{…}`](#tag/users/paths/~1{organisation_id}~1users~1me/get) in order to access and manipulate data within your Keeping organisation. For example, use:\n\n```bash\ncurl -XGET https://api.keeping.nl/v1/{organisation_id}/users/me \n -H \"Authorization: Bearer {your access token}\" \\\n -H \"Accept: application/json\"\n```\n\nto get information about your user within the selected organisation. Nice 👍, you are off to a great start. Now read 📖 along to discover how to use the Keeping API to the fullest.\n\n# Authentication\n\nTo start using the Keeping API, you will need to have an [account](https://keeping.nl/register) with Keeping, be part of an organisation within Keeping and have an access token. You can either be part of an organisation as an administrator or just a regular team member. Team members have **limited access**, and can only use a subset of the API.\n\nYou can obtain an access token in one of two ways: use a [personal access token](#section/Authentication/Personal-access-tokens) directly linked to your Keeping account,\nor implement an [OAuth2 authentication flow](#section/Authentication/OAuth2) to allow other Keeping customers to use your integration.\n\nOur OAuth2 implementation follows [RFC 6749](https://tools.ietf.org/html/rfc6749).\n\n## Personal access tokens\n\nIf you want to experiment with our API or want to write a one-off integration for yourself, you can use a **personal access token**. To acquire a personal access token:\n\n1. [Login to Keeping](https://keeping.nl/auth/login).\n2. Navigate to **My account** using the dropdown in the upper right corner, under your avatar.\n3. Navigate to **For developers** in the vertical menu on the left.\n4. Click on the green **Create access token** button next to the header: **Personal access tokens**.\n5. Select the appropriate [scopes](#section/Authentication/Scopes) you wish to use if you are not sure you can select all of them for an experimental setup.\n6. The new access token is displayed, you should copy it from the presented screen as it is only shown once.\n\nMake sure your access token functions correctly by sending the following request, using cURL or another tool.\n\n```bash\ncurl -XGET https://api.keeping.nl/v1/organisations \\\n -H \"Authorization: Bearer {your access token}\" \\\n -H \"Accept: application/json\"\n```\n\nPersonal access tokens are valid indefinitely until you revoke them yourself from the **Developer** screen within Keeping.\n\n> **Please note:** be very careful with your personal access token, somebody with access to it can quickly get access to all your data within Keeping. Do not share a personal access token with other people or paste it into external applications. External applications should use the [OAuth2](#section/Authentication/OAuth2) authentication flow.\n\nBoth personal access tokens as well as regular OAuth2 access tokens conform to the [JWT format](https://jwt.io/) and can be verified using our [public key](https://developer.keeping.nl/oauth_jwt_public.key). Access tokens should be sent along in every request after authentication. You should add them as bearer tokens in the `Authorization` HTTP header prefixed with the string `Bearer `.\n\n\n## OAuth2\n\nUse the OAuth2 authentication flow if you want to allow other Keeping customers to use the integration you are developing. Before you start authenticating a user, you should register a new OAuth2 application within Keeping yourself. To do this:\n\n1. [Login to Keeping](https://keeping.nl/auth/login).\n2. Navigate to **My account** using the dropdown in the upper right corner, under your avatar.\n3. Navigate to **For developers** in the vertical menu on the left.\n4. Click on the green **Register application** button next to the header: **Your applications (OAuth clients)**.\n5. Enter a name that describes your application clearly. This name will be displayed to users when Keeping asks them whether they grant you access to their data. \n6. Enter a redirect URL that Keeping can use to direct users to your application after they accept to share their Keeping data with you. The redirect URL could be something like: `https://ella-evenementen.nl/keeping-redirect/` or use a custom scheme like `myapp://keeping-redirect`.\n7. A new OAuth client is created, and its `client_id` and `client_secret` are shown in the table.\n\n> **Please note:** Make sure never to share the `client_secret` with other parties. When there is any indication that your `client_secret` could be compromised, you should delete the current OAuth2 client in Keeping and create a new one. Afterwards, you should swap the `client_id` and `client_secret` in your code, and distribute a new version of your software if necessary.\n\n### The OAuth2 authentication flow\n\nTo start the authentication process using OAuth2, you should direct your users to `https://keeping.nl/oauth/authorize?…` including some of the URL parameters specified below. The resulting page will ask the user to login and, once logged in, ask the user to grant your application access to their data. When a user accepts, Keeping will send them back with an **authorisation code** to the redirect URL you provided while setting up the OAuth2 client. You can use this authorization code to request an access token.\n\nIf you are not operating within the context of the web, like when you have a native desktop or mobile app, you should use a webview library or external browser for opening this URL. The redirect URL supports custom schemes, i.e. your application could provide something like `myapp://keeping-redirect` as redirect URL. \n\n**Start the process by directing your user to the URL:**\n\n```\nhttps://keeping.nl/oauth/authorize?client_id={your_apps_oauth_client_id}&redirect_uri={your_apps_redirect_uri_urlencoded}&response_type=code&scope=time+team+reporting+project_management&state={your_optional_nonce}\n```\n\nYou can provide the following URL parameters:\n\n| URL parameter for authorisation | | |\n|---|---|---|\n| `client_id` | (required) | The `client_id` for the OAuth2 client you registered as a developer with Keeping. |\n| `response_type` | (required) | Should have the value `code`, other response types are not supported. |\n| `scope` | (required) | Provide the [scopes](#section/Authentication/Scopes) you wish to use. When using multiple, chain them using the `+` character, e.g. `time+team`. _Please note: do not use `%2B` to chain scopes._ |\n| `redirect_uri` | (recommended) | Provide the exact redirect URL you setup within Keeping in [url encoded format](https://en.wikipedia.org/wiki/Percent-encoding). So if you have entered a redirect URL like: `https://ella-evenementen.nl/keeping-redirect/` you should provide the value `https%3A%2F%2Fella-evenementen.nl%2Fkeeping-redirect%2F`. |\n| `state` | (optional) | A value that will be sent along when Keeping redirects the user back to your application. You can use this to identify the result. |\n\n> **Please note:** Do not capture a user's login information or fill the Keeping login form programmatically. This is not a safe method for interacting with an OAuth2 authentication flow.\n> If we find that your application is capturing, storing or programmatically handling a user's Keeping credentials, we will disable access for your OAuth2 clients.\n\nThe authorisation screen the user will see, after login, looks something like this:\n\n![Example OAuth2 authorisation screen](https://keeping.nl/images/developer/oauth-screen.png \"Example OAuth2 authorisation screen\")\n\nIf something goes wrong, or the user denies the request, the user will be sent back to your application using the redirect URL you provided.\nTo let you know what went wrong Keeping will send along a couple of URL parameters:\n\n| URL parameters for redirect on failure | |\n|---|---|\n| `error` | Right now could be: `access_denied`, `invalid_request`, `unauthorized_client`, `unsupported_response_type`, `invalid_scope`, `server_error`, `temporarily_unavailable` |\n| `error_description` | Optional English description of the error. For now, this description is **not** localised according to the `Accept-Language` header, but might be in the future. |\n| `hint` | Optional English hint on how to solve the error. For now, this hint is **not** localised according to the `Accept-Language` header, but might be in the future. |\n| `state` | The exact same value you provided when directing the user to Keeping's authorize URL. |\n\nWhen the user clicks on **Accept** they will be sent back to your application using the redirect URL you provided, along with the **authorisation code** in the URL parameter `code`, e.g. `https://ella-evenementen.nl/keeping-redirect/?code={authorisation_code}`.\n\n| URL parameters for redirect on success | |\n|---|---|\n| `code` | The authorisation code you can use to request an access token. |\n| `state` | The exact same value you provided when directing the user to Keeping's authorize URL. |\n\nThe authorisation code is valid for **60 minutes** after it has been issued, so make sure you request an access token immediately after receiving the authorisation code.\n\n### Request an access token\n\nNow you can request the access token. Send your authorisation code and the OAuth client details in a `POST` request to `https://api.keeping.nl/v1/oauth/token` to receive an access token.\nThis request requires you to send along the `grant_type`, `code`, `client_id`, `client_secret` and the `redirect_uri`. The `code` is the just received authorisation code. \nThe `grant_type`, in this case, should have the value `authorization_code`.\nKeeping does **not** support other grant types for requesting an initial access token at the moment.\nThe `client_id`, `client_secret` and `redirect_uri` should correspond precisely with the details you provided to Keeping when registering your application.\nMake sure to package all the required parameters as form data in the request body. Read the [documentation of the actual request](#tag/oauth/paths/~1oauth~1token/post) for more information.\n\nYou could acquire an access token with cURL as follows:\n\n```bash\ncurl -XPOST https://api.keeping.nl/v1/oauth/token \\\n -H \"Accept: application/json\" \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"grant_type=authorization_code\" \\\n -d \"code={the_just_received_authorisation_code}\" \\\n -d \"client_id={your_apps_oauth_client_id}\" \\\n -d \"client_secret={your_apps_oauth_client_secret}\" \\\n -d \"redirect_uri={your_apps_oauth_redirect_url}\"\n```\n\nIf the token is requested successfully you will receive a `200` response containing both an access token and a refresh token. \n\n```json\n{\n \"token_type\": \"Bearer\",\n \"expires_in\": 86400,\n \"access_token\": \"{your_access_token}\",\n \"refresh_token\": \"{your_refresh_token}\"\n}\n```\n\nYou can now use the value of the `access_token` property to send requests to the Keeping web service using the `Authorization` header.\nMake sure to set its value to: `Bearer {your_access_token}`.\nKeeping will always return a `Bearer` token. In order to comply with the OAuth2 specification,\nKeeping makes sure to inform you about the type of token using the `token_type` response property.\n\nBoth personal access tokens as well as regular OAuth2 access tokens conform to the [JWT format](https://jwt.io/) and can be verified using our [public key](https://developer.keeping.nl/oauth_jwt_public.key).\n\nOAuth2 access tokens are usually valid for **24 hours**, but this is subject to change. The `expires_in` will provide the number of seconds the access token is valid.\n Make sure to calculate the exact moment of expiration yourself to prevent superfluous requests to the web service.\n When an access token has expired, the server will respond with a `401` HTTP status code.\n You can request a new access token using the `refresh_token`, once it has expired. \n\n### Refresh when an access token has expired\n\nWhen an access token has expired, you can request a new access token without requiring the user to follow the authentication flow again.\nYou can use the `refresh_token` you received alongside your original access token.\nRefresh tokens are valid for **90 days** from the moment they are issued, so make sure to use it in that time to keep your session alive.\n\nYou can use the [request access token](#tag/oauth/paths/~1oauth~1token/post) request similar to when you used it to acquiring the original access token to get a new one. An example using cURL:\n\n```bash\ncurl -XPOST https://api.keeping.nl/v1/oauth/token \\\n -H \"Accept: application/json\" \\\n -H \"Content-Type: application/x-www-form-urlencoded\" \\\n -d \"grant_type=refresh_token\" \\\n -d \"refresh_token={your_last_refresh_token}\" \\\n -d \"client_id={your_apps_oauth_client_id}\" \\\n -d \"client_secret={your_apps_oauth_client_secret}\" \\\n -d \"redirect_uri={your_apps_oauth_redirect_url}\" \\\n -d \"scope=time+team+reporting+project_management\"\n```\n\nYou will receive a new access token and a new refresh token, in the exact same way you received your original access token.\nYou can only use a refresh token once to get a new token.\n\n## Scopes\n\nAccess to Keeping can be limited by using not all but only one, or several scopes while requesting an access token.\nYou could do this to mitigate security risks and inform the users what specific data and functionality your integration uses.\n\n| Scope | Description |\n|---|---|\n| `time` | Allows you to manipulate time entries for yourself, and if you combine `time` with `team` you can manipulate time entries for other users as well. |\n| `reporting` | Allows you to use reporting functions for your time data. If you combine `reporting` with `team`, this will allow you to access reporting for your entire team. |\n| `team` | Allows you to request information about all users within the organisation if you are an administrator. |\n| `project_management` | Allows you to manage projects, clients and tasks if you are an administrator. |\n\n\nWhen requesting access for multiple scopes, you can chain them separated by a plus, e.g. `team+project_management`.\n\nIf you try to access a part of the API that is associated with a scope you did not specify when requesting your access token, you will receive a `403` [response](#section/General-usage/Responses) from the server.\n\n## Authorisation\n\nAside from the scopes used by your personal access token or the OAuth2 client,\nKeeping also checks a user's authorisation for each specific action based on their current role,\nthe organisations active subscription plan and specific organisation settings.\nIf one of these checks fails, you receive a `403` [response](#section/General-usage/Responses) from the server,\nand this means you do not have the correct authorisation for the action you are trying to execute.\n`401` [responses](#section/General-usage/Responses) are reserved for notifying you about invalid,\nrevoked or expired access tokens. Keeping checks your access token's validity before it checks your authorisation.\n\n### The user being a member of the organisation\n\nIf you are not a member of, or are blocked from, the organisation you try to request data of, you will receive a `403` response for all requests regarding that organisation.\nWhen an administrator archives your user, your access to that organisation is blocked. Make sure to check all your active organisations using [`GET /organisations`](#tag/organisations/paths/~1organisations/get).\n\n### The user's role\n\nUsers within an organisation have a `role` which is a property available on the `user` object. This role is either `administrator` or `team_member`.\nOnly administrators are allowed to access the scopes `team` and `project_management`.\nAnd only administrators are entitled to request reports for the entire team or manipulate the time entries of other users within the organisation.\nMake sure to check your role under using [`GET /{organisation_id}/users/me`](#tag/users/paths/~1{organisation_id}~1users~1me/get).\n\n### The organisation's active plan\n\nIf your organisation's current subscription plan does not allow a specific feature to be used you will receive a `403` response as well.\nOur detailed [pricing table](/prijzen/vergelijken) shows all the features for each specific plan. At the moment Keeping organisations can have one of the following subscription plans:\n\n| Plan | Description |\n|---|---|\n| `free_2019` | This [free plan](/prijzen/vergelijken) allows limited Keeping features to be used. It is meant for organisations with only a single user. All requests that require the `team` scope are unavailable for an organisation using this plan. |\n| Legacy: `free_2018`, `standard_2018` | These plans have the same restrictions as the `free_2019` plan. With the exception of multiple users in an organisation. Organisation's with these plans are allowed to use the requests under the `team` scope. |\n| `plus_2019` | This is our current regular paid plan and does not have restrictions, except for some on very specific undocumented enterprise features. |\n| `enterprise_2019` | This is our enterprise plan which does not have any restrictions whatsoever. Enterprise features are not reflected in this API documentation as of yet. |\n\nMake sure to check your organisation's plan using [`GET /organisations`](#tag/organisations/paths/~1organisations/get).\n\n### The organisation's feature toggles\n\nYou will also receive a `403` response if you try to use a feature which has been disabled by the organisation's administrators.\nKeeping currently has the following feature toggles available on the `organisation` object:\n\n```json\n{\n …\n \"features\": {\n \"timesheet\": \"hours\",\n \"projects\": true,\n \"tasks\": true,\n \"breaks\": true\n },\n …\n}\n```\n\nIf, for example, **projects** are disabled you will not be able to request or modify projects. On the other hand, you will not be required to specify a project when creating time entries as well.\nThe same is true for **tasks**. When **breaks** are disabled you will not be able to create new or modify existing time entries with the purpose `break`.\nSo, make sure to check your organisation's currently enabled features using [`GET /organisations`](#tag/organisations/paths/~1organisations/get) before using any of them.\n\n# General usage\n\nNow that you understand our authentication mechanisms, we will explain some API characteristics that are not specific to any request but are used throughout the entire API.\nPlease make sure to read this chapter carefully.\n\n## Usage etiquette\n\nWe provide the Keeping API to both our paid and free customers.\nIf you create an awesome integration for your software product [let us know](https://keeping.nl/contact)😄!\nAnd if you use the API to improve your internal reporting make sure to share your experience with us as well.\n\nIf you register an OAuth2 application, make sure Keeping's customers are well informed about the types of data they share with your application.\nEven though Keeping informs the user in the OAuth2 authorisation screen, we still like you to take extra care in informing them well.\nPlease note that administrators have access to the data of all users within the organisation.\nAdditionally, make sure to ask users explicitly for their permission when you want to store their Keeping data in a permanent fashion yourself.\nPlease be transparent about your data and privacy policy, and make sure to honour their rights under the [GDPR](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation).\n\nThe Keeping API does have [usage limitations](#section/General-usage/Usage-limitations) but aside from those, we also ask you not to abuse the web service.\nWe reserve the right to block anyone from using the web service or to temporally disable it altogether.\nPlease do not send an excessive amount of requests to the web service. Do not send more than `3` requests in parallel.\nDo not poll the web service periodically to check for changes—and if you absolutely need to, make sure it is once per day, during the night ([CET](https://en.wikipedia.org/wiki/Central_European_Time)).\nDo not call the web service as part of an automated testing procedure. Make sure to cache responses where appropriate.\n\nPlease send along with the name of your software, its exact version and the developer's e-mail address in the `User-Agent` header of the requests.\n\n## Requests\n\nTo send a request to the Keeping web service you can use any HTTP command-line tool such as [cURL](https://en.wikipedia.org/wiki/CURL), or any general-purpose HTTP library.\nWe have a couple of suggestions:\n\n- PHP: [Guzzle](http://docs.guzzlephp.org/en/stable/), [Httpful](https://github.com/nategood/httpful), [cURL](https://www.php.net/manual/en/book.cURL.php) or [file_get_contents()](https://www.php.net/manual/en/function.file-get-contents.php)\n- JavaScript: [axios](https://github.com/axios/axios), [superagent](https://github.com/visionmedia/superagent), [XMLHttpRequest](https://developer.mozilla.org/nl/docs/Web/API/XMLHttpRequest) or [fetch()](https://developer.mozilla.org/nl/docs/Web/API/Fetch_API)\n- Python: [Requests](https://2.python-requests.org//en/latest/index.html), [httplib2](https://github.com/httplib2/httplib2) or [http.client](https://docs.python.org/3/library/http.client.html)\n- Ruby: [REST Client](https://github.com/rest-client/rest-client) or [Active Resource](https://github.com/rails/activeresource)\n- Swift: [Alamofire](https://github.com/Alamofire/Alamofire) (or [AFNetworking](https://github.com/AFNetworking/AFNetworking) for Objective-C)\n- Java/Kotlin: [OkHttp](https://square.github.io/okhttp/)\n\n\n### Request base URL\n\nAll the URL path's for the API are constructed as follows:\n\n```\nhttps://api.keeping.nl/{version}/{path}\n```\n\nWhere `{version}` is the API version, right now only `v1` is available and is described in this document.\nThe `{path}` should be swapped with the specific path of the API you wish to use.\nMost paths start with `{organisation_id}/` for the relevant Keeping organisation.\n\n### Request methods\n\nRequests made to the Keeping web service can be made using one of the following four HTTP methods.\n\n| Method | Description |\n|---|---|\n| GET | These requests are used to fetch lists of data instances or a single data instance. |\n| POST | These requests are used to create new data instances. |\n| PATCH | These requests are used to mutate data instances by either updating properties or executing a mutative command. |\n| DELETE | These requests are used to permanently delete data instances. |\n\nThe Keeping API does **not** support `PUT`, `HEAD`, `OPTIONS` or other unspecified HTTP methods. \n\n### Request headers\n\nRequests sent to the web service should contain at least two headers: `Authorization` and `Accept`.\nSome requests also require you to send the `Content-Type` header. The API supports the following HTTP request headers:\n\n| Header | | Description |\n|---|---|---|\n| Authorization | **required** | This header should contain the access token in the form of: `Bearer {your access token}`. Requests used for the authentication flow are the exception and do not require the `Authorization` header. |\n| Accept | **required** | Describes the requested data format of the response body. This header should always have `application/json` as its value. |\n| Content-Type | **required** if the request contains a body | Describes the data format of the request body. This header should have the value `application/json` when you are submitting a JSON body with a `POST` or `PATCH` request. Only the [OAuth requests](#tag/oauth/paths/~1oauth~1token/post) use `application/x-www-form-urlencoded`. |\n| Accept-Language | _optional_, default: `en` | This header can be used to specify the preferred response language for strings containing natural language. Keeping currently supports: `nl` and `en`. When not provided, Keeping assumes `en` as this is generally the language developers work with while programming. Keeping does **not** support constructed `Accept-Language` header values with priorities and multiple languages or locales. |\n| User-Agent | _optional_ | Used to provide Keeping with information about your application, its version and your contact details. |\n\n### Request parameters and body\n\nSome `GET` requests will allow you to specify additional parameters.\nYou should pass these along in the URL as query parameters, for example by appending `?first=value1&second=value2` to the URL.\n\nSome `POST` and `PATCH` requests require you send information in the form of a JSON request body.\nUnder 'Response body' you will find the specific JSON structure for each request. Usually, not all properties are required, and thus you are free to leave out optional properties from the root JSON object, or nested objects.\nMake sure the data is encoded in the type specified in the API, i.e. when a string is expected make sure to send a string, when an integer is expected make sure to send an integer.\nWhen packaging your data in the request body make sure to also include the `Content-Type: application/json` header in the request.\nThe API does **not** support other content type for encoding the request body, except for the [OAuth requests](#tag/oauth/paths/~1oauth~1token/post) which uses `application/x-www-form-urlencoded`.\n\n## Responses\n\nWeb service responses start with an HTTP status code. Before doing anything with the response body, first, check this code to determine roughly what happened.\n The Keeping web service can return responses with one of the following HTTP status codes:\n\n| Code | Meaning | Description |\n|---|---|---|\n| 200 | OK | You have successfully received data from the web service through a `GET` request or successfully executed a mutation through a `PATCH` request. |\n| 201 | Created | You have successfully created new data through a `POST` request. |\n| 204 | No content | You have successfully deleted something through a `DELETE` request. |\n| 400 | Bad request | Keeping is unable to process the request because the request itself or its body contents is invalid. This could happen when the request body does not contain valid JSON, is incomplete, or has the wrong JSON structure. |\n| 401 | Unauthorized | This status code will be used if the token sent along in the `Authentication` header is invalid, has been revoked or has expired. When you receive a 401 you should either request a new token with the refresh token or reauthenticate. |\n| 403 | Forbidden | You are not allowed to use this part of the API, or are not allowed to use a specific instance specified in a URL parameter or request body. You could also not have enough privileges within the selected organisation, e.g. because you are not an admin but just a team member, or because the used OAuth2 client does not provide access to the scope required. Your IP address could have been blocked from using the web service as well, e.g. by systematically ignoring the [usage limitations](#section/General-usage/Usage-limitations). |\n| 404 | Not found | You are trying to access or reference data that does not exist (anymore). This could also concern a specific instance specified in a URL parameter or the request body. When previously you received data of a specific instance on the same API path, you can safely assume that specific instance has been deleted. |\n| 405 | Method not allowed | You sent a request to a URL path with a method which is not supported for that specific path, e.g. you sent a `POST` request to a path that only supports the `GET` method. | \n| 422 | Unprocessable entity | Validation of a URL parameter or data in the request body has failed. Please check the response body for more information. |\n| 429 | Too many requests | You have exceeded the [usage limitations](#section/General-usage/Usage-limitations). Make sure to stop calling the web service entirely for at least the amount of seconds specified in the `Retry-After` response header. |\n| 500 | Internal server error | Something went wrong within Keeping with your specific request. We get automatically notified of these errors, but please feel free to reach out to [support@keeping.nl](mailto:support@keeping.nl) for further information. |\n| 502 | Bad gateway | There is no server available to handle your request. This could happen when Keeping is experiencing unexpected high load. Please check [status.keeping.nl](https://status.keeping.nl) for more information. |\n| 503 | Service unavailable | Keeping web service has been taken offline. Sometimes this can happen during a deploy for a couple of seconds. This could also be because of planned maintenance. Please check [status.keeping.nl](https://status.keeping.nl) for more information. |\n| 504 | Gateway timeout | The server handling you request was taking too long to handle it. This could happen when Keeping is experiencing unexpected high load. Please check [status.keeping.nl](https://status.keeping.nl) for more information. |\n\n### Response body\n\nWhen a response contains a JSON body, the response header `Content-Type: application/json` is present. In general, responses with a `2xx` status code are accompanied by a JSON body with the following structure:\n\n```json\n{\n \"{instance_name}\" : { // Contains either a single object or an array of objects\n …\n },\n \"meta\" : { // Optional additional information\n …\n }\n}\n```\n\nThe object or array under the `{instance_name}` keypath contains the main requested, mutated or created data. For example, when requesting time entries this will be `time-entries`.\nIn response to a successful `DELETE` request, the value of `{instance_name}` will be `null`.\nThe information present in the optional object under `meta` keypath contains other useful information, e.g. regarding pagination.\nIf applicable, Keeping will present strings in the natural language requested using the `Accept-Language` request header.\n\nResponses with a `4xx` or `5xx` **can** be accompanied by a JSON body with the following structure:\n\n```json\n{\n \"error\" : {\n \"message\" : \"Oops… something went wrong.\",\n … // Additional information could be given,\n // e.g. in the case of a 422: Unprocessable entity\n }\n}\n```\n\nPlease do not assume the existence of an accompanied JSON encoded error message, it could very well be that our load balancer returns an HTML document or something else entirely.\nSo when handling error responses, first check the status code and only if present look at the JSON encoded error message for more information.\nKeeping will try to present the error message in the language requested using the `Accept-Language` request header.\nData from the error message is not suitable for use in error handling logic other than displaying a message to the user, and should always be considered subject to change.\n\n\n## Pagination\n\nIn some cases, the API returns a **paginated** array of instances, for example with `GET …/{organisation_id}/projects`.\nThe array has a default length of `25` instances per request. To request the next `25` instances you will need to make an additional request for the next _page_.\nYou do this by adding the `?page=2` URL parameter. Pages are indexed starting with one, i.e. the first page is `page=1`. So, `page=0` does not exist.\nThe root JSON object returned by these paginated requests contains information regarding pagination under the `meta` keypath. \n\n```json\n{\n \"{instance_name}\": [\n …\n ],\n \"meta\": {\n \"total\": 115,\n \"per_page\": 25,\n \"current_page\": 1,\n \"last_page\": 5,\n …\n }\n}\n```\n\nYou can also add the `per_page` URL parameter to modify the number of instances returned by Keeping. The maximum value is `100`.\n\n## Dates and time zones\n\nDates will be returned in the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date format.\nWhere a property is representing a specific moment in **time** the value will return a string including seconds and time zone adjustment, e.g.: `2019-11-27T20:45:10+02:00`.\nThis corresponds to the PHP date format: `Y-m-d\\TH:i:sP`.\nWhere a property is representing just a specific **day**, irrespective of the exact moment on that day or the timezone,\nKeeping will return/require a string in the PHP date format: `Y-m-d`, e.g. `2019-11-27`.\n\nTime zones in Keeping are set per organisation. You can [change the time zone](https://keeping.nl/support/instellingen/tijdzone-aanpassen) in the organisation settings.\nThe `organisation` object does contain a `time_zone` property, the string value corresponds with the [tz database name](https://en.wikipedia.org/wiki/Tz_database) for the selected time zone.\nTimes outside of the scope of an organisation will be returned in [Coordinated Universal Time](https://en.wikipedia.org/wiki/Coordinated_Universal_Time), i.e. `UTC`.\nFor now, the web service has **no possibility** of returning dates in a representation with another time zone than set for the organisation, neither by setting a user-specific time zone, nor by sending the web service a special time zone header.\n\n## Server time\n\nThe current time on the Keeping servers is synced using [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol) to make sure it is accurate.\nKeeping uses the NTP pool at: `ntp.ubuntu.com`.\nTo make sure you know the server time at the moment of processing your request Keeping sends along a special header in the response: `X-Server-Time-Ms`.\nThe value is the current [Unix Time](https://en.wikipedia.org/wiki/Unix_time) on the server expressed in **milliseconds**.\nUse this time to calculate the offset between your machines current time and the Keeping server time to correctly calculate hours for running time entries.\nWhen calculating the offset between your local machine time and the server time make sure to account for the average time requests are in transit. \n\n## Usage limitations\n\nAside from asking to conform to the [usage etiquette](#section/General-usage/Usage-etiquette), the Keeping API also throttles requests using a rate limiter to prevent abuse.\nYou can make `50` requests to the API every `1` minute for each individual Keeping account that is either associated with the used personal access token or the OAuth2 session, i.e. having multiple personal access tokens or OAuth2 sessions does not increase your rate limit.\n\nAll web service responses will contain the header `X-RateLimit-Limit` with the value `50` for the limit.\nResponses also contain the header `X-RateLimit-Remaining` with the number of remaining requests, e.g. `49` after one request.\n\nOnce you have hit the rate limit, and you receive a `429`, the provided response header `Retry-After` specifies the number of seconds you should wait.\nIts value will be `60`. You will also receive the `X-RateLimit-Reset` header, specifying the [Unix time](https://en.wikipedia.org/wiki/Unix_time) in seconds when you are allowed to retry.\nWhen that time passed, the number of requests remaining will have been reset to `50`. These values are subject to change, so make sure to implement the handling of hitting the rate limiter using the actual header values.\n\n## Miscellaneous \n\nThis API does **not** feature [Cross-Origin Resource Sharing (CORS)](https://www.w3.org/TR/cors/).\n\nWe use a lot of [open-source software at Keeping](https://keeping.nl/open-source-attribution).\nOne such piece of software is [ReDoc](https://github.com/Redocly/redoc) which was used to generate our [documentation website](https://developer.keeping.nl) from our [OpenAPI 3.0 specification](https://developer.keeping.nl/openapi.json).\n",
"termsOfService": "https://keeping.nl/terms",
"contact": {
"name": "Support team",
"email": "support@keeping.nl"
}
},
"servers": [
{
"url": "https://api.keeping.nl/v1"
}
],
"externalDocs": {
"description": "Find more information in our knowledge base",
"url": "https://keeping.nl/support"
},
"x-tagGroups": [
{
"name": "General",
"tags": [
"oauth",
"organisations"
]
},
{
"name": "Time",
"tags": [
"entries"
]
},
{
"name": "Reporting",
"tags": [
"reports"
]
},
{
"name": "Team",
"tags": [
"users"
]
},
{
"name": "Project Management / Settings",
"tags": [
"projects",
"tasks",
"clients"
]
}
],
"tags": [
{
"name": "oauth",
"x-displayName": "Access token",
"description": "In this section, you will find documentation for requesting authentication information. Please read [this section](#section/Authentication/OAuth2) for more information on the OAuth2 authentication flow and the acquiring of an access token. Our implementation follows [RFC 6749](https://tools.ietf.org/html/rfc6749).\n\n\n| Types of tokens/codes | Valid for | Remark |\n|---|---|---|\n| Authorization code | 60 minutes | Can only be used once. |\n| Access token | 24 hours | The `expires_in` will provide the number of seconds the access token is valid. |\n| Refresh token | 90 days | Can only be used once. You'll receive a new refresh token when using it. |\n"
},
{
"name": "organisations",
"x-displayName": "Organisations",
"description": "When you use Keeping, you always do so in the context of a specific organisation. You can see one of Keeping's organisations as a workspace in which everything is contained, and usage takes place. Data from one organisation (e.g. time entries or projects) cannot be accessed or used in another organisation. \n\nYour Keeping account can be associated with multiple organisations, and so you yourself can be a part of multiple. When you start using Keeping, before you do anything else, you should choose which organisation you want to use. Within each organisation you have your own `user` object which can be accessed through: [`GET /{organisation_id}/users/me`](#tag/users/paths/~1{organisation_id}~1users~1me/get).\n\nTherefore, before you start using the API, you should select the organisation you want to use. The paths of all further requests are prefixed with `{organisation_id}`, in which you should specify the selected organisation. To get an overview of your options, you can [fetch all your currently active organisations](#tag/organisations/paths/~1organisations/get).\n\nThere could also be some inactive organisations. These are disabled organisations that are planned for deletion, or organisations to which your access has been blocked. You are denied access when an administrator archives your user. The API does not provide the possibility to fetch inactive organisations.\n\n\n"
},
{
"name": "entries",
"x-displayName": "Time entries",
"description": "Use the calls below to manipulate time entries in Keeping. APIs described in this section correspond roughly with the actions you can yourself take when interacting with Keeping's timesheet, either through the app or through the website.\n\nTo use these, make sure your session has access to the `time` scope. Regular team members can only access their own time entries, in that case, you should use your own `user_id`, which you can retrieve through [`GET /{organisation_id}/users/me`](#tag/users/paths/~1{organisation_id}~1users~1me/get). If you are an administrator **and** have access to the `team` scope, you can also fetch and manipulate other user's time entries.\n"
},
{
"name": "reports",
"x-displayName": "Reports",
"description": "Use these APIs to retrieve reporting data from Keeping. If you want to use the hours tracked in another application, but you wish to track hours in Keeping, you should use these APIs instead of the time APIs.\n\nMake sure you have access to the `reporting` scope. If your user is an `administrator`, you have access to all-time reports for the entire organisation. When your user is a `team_member` you'll only have access to reports on your own time entries. When you have this limited role, make sure always to pass the URL query parameter: `?user_ids[]={your_user_id}` and no additional user id, otherwise the web service will respond with a `403`.\n \nThere are just two URL paths used for reporting. That may seem little, however, these are both flexible and powerful due to the vast number of possible parameter combinations. You can either get an [overview](#tag/reports/paths/~1{organisation_id}~1report/get) of the hours in a specific period for a particular predicate or get the [underlying time entries](#tag/reports/paths/~1{organisation_id}~1report~1time-entries/get).\n"
},
{
"name": "users",
"x-displayName": "Users",
"description": "When you have access to the `team` scope and you are an `administrator` you can retrieve the information of your entire team. However, you can always access the information of [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get), even without the `team` scope.\n\nIt is perhaps important to realise that a single Keeping account, represented by a set of login credentials, can be associated with **multiple** users entities—one per organisation. So a user within the context of this API should be considered: \"a user of an organisation's Keeping environment\". This means that, just as other entities, users are scoped to the selected organisation. Therefore you can have multiple \"me\" users for your current API session, a different one per organisation.\n"
},
{
"name": "projects",
"x-displayName": "Projects",
"description": "Fetch and manage your projects when `projects` are enabled.\n\nYou can always fetch the projects you can use for creating your own time entries.\n\nYou can create, edit and manage projects only when your user is an `administrator` and the session has access to the `project_management` scope.\n\nProjects are directly linked to clients, when you have enabled `projects` you automatically have access to clients as well, and similarly when `projects` are disabled clients are as well.\n\n<!--- just to make sure, is it intended to use the term 'participations' here instead of 'participants'? I know that this is the database term but participations seems like a weird term to describe the users. -->\nThe ability to create time entries for specific projects by certain team members can be restricted by the usage of `participations`, which indicate who can create time entries for that project. And if both `tasks` and `projects` are enabled for your organisation, you have the ability to limit the usage of specific tasks to certain projects through the usage or `task_assignments`. Thus, a project's `participations` tell you which users can create time entries with it, and a project's `task_assignments` tell you which tasks can be used for time entries in combination with the project.\n\nA created project starts out as an `active` project, but can be changed to `archived` when you [archive](#tag/projects/paths/~1{organisation_id}~1projects~1{project_id}~1archive/patch) it. You can only [delete projects](#tag/projects/paths/~1{organisation_id}~1projects~1{project_id}/delete) that do **not** have any time entries associated with them. \n"
},
{
"name": "tasks",
"x-displayName": "Tasks",
"description": "Fetch and manage your tasks when `tasks` are enabled.\n\nYou can always fetch all tasks. However, you can only create, edit and manage tasks when your user is an `administrator` and the session has access to the `project_management` scope.\n\nYou can use tasks to classify time entries to specific types of tasks or activities. For example you could have a task for: Recruitment, HR, Administration, Acquisition, Project Management, Marketing, etc. So the concept of a task in Keeping does not necessarily relate to an individual to-do but rather a classification for the type of to-do.\n\nWhen you use tasks in conjunction with `projects`, you can specify per project which tasks can be performed while working on it. You do this using the `task_assignments` property on the `project` object. In this case you can only create time entries using an active task assigned to the selected project.\n\nIf you do not use `projects` you can create time entries using all active tasks. A created task starts out as an `active` task, but can be changed to `archived` when you [archive](#tag/tasks/paths/~1{organisation_id}~1tasks~1{task_id}~1archive/patch) it. You can only [delete a task](#tag/tasks/paths/~1{organisation_id}~1tasks~1{task_id}/delete) that does **not** have any time entries associated with it. \n"
},
{
"name": "clients",
"x-displayName": "Clients",
"description": "Manage your clients when `projects` are enabled for your organisation, your user is an `administrator` and the session has access to the `project_management` scope. \n\nClients are used within Keeping primarily to group projects within the interface and for the reports. You cannot directly create time entries for a client, this has to be done through it's projects. So, clients can have projects linked to them. Therefor when you have enabled `projects` you automatically have access to clients as well, and similarly when `projects` are disabled clients are as well.\n\nA created client starts out as an `active` client, but is changed to `inactive` when it only has archived projects associated with it. You can only [delete a client](#tag/clients/paths/~1{organisation_id}~1clients~1{client_id}/delete) that does **not** have any projects associated with it. \n"
}
],
"paths": {
"/oauth/token": {
"post": {
"summary": "Acquire an access token",
"tags": [
"oauth"
],
"requestBody": {
"required": true,
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/oauth_request"
},
"examples": {
"authorization_code": {
"summary": "Using an authorisation code",
"value": {
"grant_type": "authorization_code",
"code": "b9ad932293dabe3bb9133225b5a71667f…",
"client_id": "12345",
"client_secret": "mQ1dY4hcAibu9MnKsstBRb5rxKiugQpceYK1FNNl",
"redirect_uri": "https://ella-evenementen.nl/keeping-redirect/"
}
},
"refresh_token": {
"summary": "Using a refresh token",
"value": {
"grant_type": "refresh_token",
"refresh_token": "b9ad932293dabe3bb9133225b5a71667f…",
"client_id": "12345",
"client_secret": "mQ1dY4hcAibu9MnKsstBRb5rxKiugQpceYK1FNNl",
"scope": "time"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauth_access_token"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/oauth_error"
}
}
}
}
}
}
},
"/organisations": {
"get": {
"summary": "Your active organisations",
"description": "Lists all your currently active organisations. Select one to proceed using the API. \n\n The `organisation` objects you receive in the response contains important information on the specific features that are available to them. Make sure you check them to ensure the correct usage of the API. You can read more details about the organisation's features in the [authorisation](http://localhost:8000/developer#section/Authentication/Authorisation) section.\n\nThe response is **not** paginated. ",
"tags": [
"organisations"
],
"security": [
{
"auth": []
}
],
"responses": {
"200": {
"description": "Successfully fetched organisations",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"organisations": {
"type": "array",
"description": "A list of all the active organisations where you (the authenticated user) are currently a part of.",
"items": {
"allOf": [
{
"$ref": "#/components/schemas/organisation"
}
]
}
}
}
}
}
}
}
}
}
},
"/{organisation_id}/users": {
"get": {
"summary": "List users",
"description": "List all users for an organisation.\nIn order to do this your OAuth session should have access to the `team` scope, and your own user should be an `administrator`.\n\nThis result is **paginated**, so you will need to send the server multiple requests if you want to\nretrieve all users when there are more than `100`.",
"tags": [
"users"
],
"security": [
{
"auth": [
"team"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"$ref": "#/components/parameters/page"
},
{
"$ref": "#/components/parameters/per_page"
},
{
"name": "state",
"description": "",
"in": "query",
"schema": {
"type": "array",
"default": [
"active",
"inactive"
],
"items": {
"type": "string",
"enum": [
"needs_invite",
"invited",
"active",
"inactive",
"blocked",
"decoupled"
]
}
},
"explode": true
},
{
"name": "project_id",
"description": "Can only be used if `projects` are enabled. Filter the users down to the ones that are participating in this specific project.\nWhen not provided Keeping will return the users for all projects, including the ones that are not participating at all.",
"in": "query",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Successfully retrieved a list of users",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"$ref": "#/components/schemas/user"
}
},
"meta": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/pagination_data"
}
]
}
}
}
}
}
},
"403": {
"description": "You are not allowed access to these users",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/users/me": {
"get": {
"summary": "Retrieve my user",
"description": "Fetch the organisation's user for the current OAuth session or the currently used personal access token.\n\nYou have a different \"me\" user entity for each organisation you have access to. Therefore, your OAuth session or personal access token can have access to **multiple** \"me\" users entities. \nFor each organisation you should use a different `user_id` to describe your user in that organisation's context. \n\nFetching your own user's data can be done even if the OAuth session does not have access to the `team` scope.",
"tags": [
"users"
],
"security": [
{
"auth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"200": {
"description": "Successfully retrieved your user",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"user": {
"$ref": "#/components/schemas/user"
}
}
}
}
}
},
"403": {
"description": "You are not allowed access to your user",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/users/{user_id}": {
"get": {
"summary": "Retrieve a user",
"description": "Fetch a single user's data from the organisation. Use this method to fetch a user other than your own user. \nIn order to do so your OAuth session should have access to the `team` scope, and your own user should be an `administrator`.",
"tags": [
"users"
],
"security": [
{
"auth": [
"team"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"200": {
"description": "Successfully retrieved user",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"user": {
"$ref": "#/components/schemas/user"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to access this user",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "User not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/time-entries": {
"get": {
"summary": "Timesheet for a user",
"description": "Fetch all time entries for a single user on a specific day. The response is **not** paginated.\n\nDo **not** use this to search through many entries.\nYou can use '[time entries report](#tag/reports/paths/~1{organisation_id}~1report~1time-entries/get)' for retrieving many time entries for different users and in an extensive date range.",
"tags": [
"entries"
],
"security": [
{
"auth": [
"time"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"name": "user_id",
"description": "The `id` of the user you want to receive time entries for.\nDefaults to [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get) when the parameter is not provided on the request.\n\nIf you do not have access to the `team` scope you can only receive time entries for [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get).\nIf you want to fetch time entries of other users, make sure you have access to the `team` scope and you are an `administrator`.",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "date",
"description": "The day you want to receive the user's time entries for. The format is: `Y-m-d`.\nDefaults to today when the parameter is not provided on the request.\n\nRead the [section on dates](#section/General-usage/Dates-and-time-zones) for more information.",
"in": "query",
"schema": {
"type": "string",
"format": "date",
"default": "2022-04-05 (today)"
}
}
],
"responses": {
"200": {
"description": "Successfully fetched time entries",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"time_entries": {
"type": "array",
"description": "The time entries for the user on the selected day.",
"items": {
"allOf": [
{
"$ref": "#/components/schemas/entry"
}
]
}
},
"meta": {
"type": "object",
"properties": {
"user_id": {
"type": "integer",
"example": 789456
},
"date": {
"type": "string",
"format": "date",
"example": "2022-04-05"
}
}
}
}
}
}
}
},
"403": {
"description": "You are not allowed to retrieve the time entries",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"post": {
"summary": "Create a time entry",
"description": "Creating a new time entry functions in a specific way depending on your organisation's `timesheet` feature setting. When you\nhave set this feature to `times`, you create time entries with a `start` and `end` time in a timeline. When you try to create such\na time entry, the response will always result in either an error or a `201` (describing the newly created entry).\n\nWhen your organisation's `timesheet` feature setting is set to `hours`, it can be that a new entry is created (`201`) or an existing entry is modified (`200`) when you try to create one.\nKeeping will make sure you will not have multiple entries with the exact same details when your `timesheet` is set to `hours`.\nSo when you create a new entry for two hours with the exact same details as an already existing entry with three hours,\nKeeping will adjust the existing entry and increment its hours to five.\n\nWhen you have either `projects` or `tasks` enabled in your organisation you are **required** to provide a valid `project_id` or `task_id` respectively.\n\nIf your organisation uses breaks and times you should take extra care interpreting the data of the created entry because they could differ from the data you provided in the request body.\nRead the documentation of the `purpose` parameter for more information.",
"tags": [
"entries"
],
"security": [
{
"auth": [
"time"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/entry_create_request"
}
}
}
},
"responses": {
"200": {
"description": "Successfully modified a time entry",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"time_entry": {
"allOf": [
{
"$ref": "#/components/schemas/entry"
}
],
"description": "The modified time entry that already exists. You than receive a `200` when the timesheet is set to `hours` and most of your\nsubmitted properties exactly match that of an existing entry.\n\nFor example, when you create a new entry for two hours with the exact same details as an already existing entry with three hours,\nKeeping will adjust the existing entry and increment its hours to five."
},
"meta": {
"type": "object",
"properties": {
"created_additional_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where created as well next to the entry referenced in `time_entry`.\nThis can happen when your new create a `break`, or the created entry was wrapped around an existing `break`."
},
"modified_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [
456789123
],
"description": "A list of time entries that where modified by the creation of the new entry. This can happen when you create a `break`,\nor an ongoing entry is stopped by the creation of another ongoing entry."
},
"deleted_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where deleted by the creation of the new entry. This can happen when you create a `break`."
}
}
}
}
}
}
}
},
"201": {
"description": "Successfully created a time entry",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"time_entry": {
"allOf": [
{
"$ref": "#/components/schemas/entry"
}
],
"description": "The newly created time entry."
},
"meta": {
"type": "object",
"properties": {
"created_additional_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where created as well next to the entry referenced in `time_entry`.\nThis can happen when your new create a `break`, or the created entry was wrapped around an existing `break`."
},
"modified_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where modified by the creation of the new entry. This can happen when you create a `break`,\nor an ongoing entry is stopped by the creation of another ongoing entry."
},
"deleted_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where deleted by the creation of the new entry. This can happen when you create a `break`."
}
}
}
}
}
}
}
},
"403": {
"description": "You are not allowed to create this time entry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"422": {
"description": "Validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/time-entries/last": {
"get": {
"summary": "Retrieve the last time entry",
"description": "Fetch the last time entry that satisfies some specific predicate.\nFor example you can fetch the last finished entry that can be resumed of a specific project using the parameters `?project_ids[]={project_id}&ongoing=false&locked=false`.\nOr you could fetch the last ongoing entry using the parameter `?ongoing=true`.\n\nPlease only use this to lookup data for suggestions and ongoing indicators, but **not** to search for many entries at once.\nYou can use '[time entries report](#tag/reports/paths/~1{organisation_id}~1report~1time-entries/get)' for retrieving many time entries for different users and in a large date range.",
"tags": [
"entries"
],
"security": [
{
"auth": [
"time"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"name": "user_id",
"description": "The `id` of the user you want to receive time entries for.\nDefaults to [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get) when the parameter is not provided on the request.\n\nIf you do not have access to the `team` scope you can only receive time entries for [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get).\nIf you want to fetch time entries of other users, make sure your access token grants you access to the `team` scope and your user has the `administrator` role.",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "date",
"description": "If you only want the last entry on a specific date. Otherwise Keeping will consider all the user's time entries.",
"in": "query",
"schema": {
"type": "string",
"format": "date"
}
},
{
"name": "purpose",
"description": "If you only want the last entry of a specific purpose.",
"in": "query",
"schema": {
"type": "string",
"enum": [
"work",
"break"
]
}
},
{
"name": "project_ids",
"description": "If you only want the last entry that is linked to **one of several** projects. You can specify the project ids, even if its just one. This parameter is only considered when `projects` are enabled. If the project does not exist, Keeping will respond with a `404`.",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"explode": true
},
{
"name": "client_ids",
"description": "If you only want the last entry that is linked to **one of several** clients. You can specify the client ids, even if its just one. This parameter is only considered when `projects` are enabled. If the client does not exist Keeping will respond with a `404`.",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"explode": true
},
{
"name": "task_ids",
"description": "If you only want the last entry that is linked to **one of several** tasks. You can specify the task ids, even if its just one. This parameter is only considered when `tasks` are enabled. If the task does not exist Keeping will respond with a `404`.",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"explode": true
},
{
"name": "external_reference_ids",
"description": "If you only want the last entry that is linked to **one of several** external references.\nYou can specify the external reference ids, even if its just one.\nIf the external reference id does not exist within Keeping it will be ignored.",
"in": "query",
"schema": {
"type": "array",
"items": {
"pattern": "^[0-9a-f]{10,40}$",
"type": "string"
}
},
"explode": true
},
{
"name": "external_reference_types",
"description": "If you only want the last entry that is linked to **one of several** external references types. You can specify the external reference types, even if its just one.",
"in": "query",
"schema": {
"type": "array",
"items": {
"enum": [
"basecamp_1_todo",
"basecamp_2_todo",
"basecamp_3_todo",
"trello_card",
"asana_task",
"github_issue",
"jira_issue",
"todoist_todo",
"generic_work_reference",
"moneybird_invoice",
"exact_online_invoice",
"jortt_invoice",
"eboekhouden_invoice",
"twinfield_invoice",
"snelstart_invoice"
],
"type": "string"
}
},
"explode": true
},
{
"name": "ongoing",
"description": "If you are only interested in the currently ongoing entry you should set this value to `true`, and if you want to ignore ongoing entries set this value to `false`.",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "locked",
"description": "If you want to ignore locked entries, set this value to `false`.",
"in": "query",
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "Successfully fetched a time entry",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"time_entry": {
"allOf": [
{
"$ref": "#/components/schemas/entry"
}
],
"description": "The last entry of the user that satisfies the provided criteria."
}
}
}
}
}
},
"403": {
"description": "You are not allowed to retrieve the entry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "No entry found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/time-entries/{entry_id}": {
"get": {
"summary": "Retrieve a time entry",
"tags": [
"entries"
],
"security": [
{
"auth": [
"time"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"name": "entry_id",
"description": "The `id` of the entry you want to fetch.\n\nIf you do not have access to the `team` scope you can only receive time entries for [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get).\nIf you want to fetch or modify time entries of other users, make sure your access token grants you access to the `team` scope and your user has the `administrator` role.",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Successfully fetched a time entry",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"time_entry": {
"$ref": "#/components/schemas/entry"
}
}
}
}
}
},
"403": {
"description": "You are not allowed access",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "No entry found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"patch": {
"summary": "Update a time entry",
"description": "Modify an existing time entry.\nYou should `PATCH` the entry, i.e. send along only the properties you wish to change in the request body.\nYou can leave out the properties you do not wish to modify.\n\nPlease note that you cannot modify locked time entries, and trying to do so will result in a `403`.\n\nWhen your organisation's `timesheet` feature setting is set to `hours`, it can be that an entry is collapsed into the entry you are modifying, and is deleted.\nThis is because Keeping will make sure you do not have multiple entries with the exact same details when your `timesheet` is set to `hours`.\nSo when you modify an entry of two hours to have the exact same details as an already existing entry with three hours,\nKeeping will collapse the three hour long entry into the two hour long entry and increment its hours to five.\nAfterwards the three hour long entry is deleted, so will be added to `meta.deleted_existing_time_entry_ids`.\n\n### Overlap prevention for breaks\nIf your organisation uses `breaks` and `times` you should take extra care interpreting the details of the modified entry\nsince they could be different from the details provided in the request body.\n\nWhen your organisation's timesheet type is set to `times` breaks can exhibit **special behaviour** which prevents them\nfrom overlapping with time entries with another purpose.\n\nYou can modify breaks such that it would overlap with `work` time entries.\nHowever, Keeping will automatically adjust the existing time entries so that the break is modified according to your specification without any overlap.\nWhen other entries get modified or deleted because of a changed break,\nKeeping will notify you using `meta.modified_existing_time_entry_ids` or `meta.deleted_existing_time_entry_ids` respectively.\n\nBreaks can overlap with other breaks but will be marked with an alert in Keeping's interface.\n\nWhen you modify a time entry that is not a `break` and there already exists a break which will overlap with that entry,\nthen Keeping will adjust the `start` or `end` of your modified entry to prevent overlap.\nKeeping could also split your modified entry in multiple entries to prevent overlap,\nin which case you receive one of them in `time_entry` and references to the others in `meta.created_additional_time_entry_ids`.\nIf the modification of a time entry is impossible due to the full overlap with a break,\nor if the modification would require you to adjust a locked entry, you will receive a `403`.",
"tags": [
"entries"
],
"security": [
{
"auth": [
"time"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"name": "entry_id",
"description": "The `id` of the entry you want to update.\n\nIf you do not have access to the `team` scope you can only receive time entries for [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get).\nIf you want to fetch or modify time entries of other users, make sure your access token grants you access to the `team` scope and your user has the `administrator` role.",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/entry_edit_request"
}
}
}
},
"responses": {
"200": {
"description": "Successfully modified the time entry",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"time_entry": {
"allOf": [
{
"$ref": "#/components/schemas/entry"
}
],
"description": "The modified time entry."
},
"meta": {
"type": "object",
"properties": {
"created_additional_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where created as well next to the entry referenced in `time_entry`.\nThis can happen when you modify a `break`, or modified an entry that was wrapped around an existing `break`."
},
"modified_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [
456789123
],
"description": "A list of time entries that where modified by the modification of the entry referenced in `time_entry`. This can happen when you modify a `break`."
},
"deleted_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where deleted by the modification of the entry referenced in `time_entry`. This can happen when you create a `break`,\nor when several entries are collapsed into a single entry on the `hours` timesheet because they now have the exact same details.\nEntries will always get collapsed into the entry you are trying to modify."
}
}
}
}
}
}
}
},
"403": {
"description": "You are not allowed to update this time entry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "No entry found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"422": {
"description": "Validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"delete": {
"summary": "Delete a time entry",
"description": "Permanently delete a time entry. Please note that you cannot delete locked time entries, and trying to do so will result in a `403`.",
"tags": [
"entries"
],
"security": [
{
"auth": [
"time"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"name": "entry_id",
"description": "The `id` of the entry you want to destroy.\n\nIf you do not have access to the `team` scope you can only receive time entries for [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get).\nIf you want to fetch or modify time entries of other users, make sure your access token grants you access to the `team` scope and your user has the `administrator` role.",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"204": {
"description": "Successfully deleted the time entry"
},
"403": {
"description": "You are not allowed to delete this entry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "No entry found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/time-entries/{entry_id}/resume": {
"post": {
"summary": "Resume a time entry",
"description": "Is a special variation of [time entry creation](#tag/entries/paths/~1{organisation_id}~1time-entries/post).\nUses all the details from the entry referenced by the path parameter `{entry_id}`\nto result in a (new or modified) ongoing entry on the current day, i.e. today.\nKeeping will indicate a new entry with a `201` status code, and a modified entry with a `200` status code.\n \nCould result in a newly created ongoing entry on the current day,\nwith the exact same details as the entry referenced by the path parameter `{entry_id}`. Or, could result\nin a modified entry in specific situations when you using the `hours` timesheet.\n\nPlease note that you cannot resume locked time entries, and trying to do so will result in a `403`.\n\nIf your organisation uses `breaks` and `times` you should take extra care interpreting the details of the resulting entry.\nRead the documentation of the `purpose` parameter on the [create request](#tag/entries/paths/~1{organisation_id}~1time-entries/post) and the documentation of the response schema under `meta` for more information.",
"tags": [
"entries"
],
"security": [
{
"auth": [
"time"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"name": "entry_id",
"description": "The `id` of the entry you want to use as a blueprint for the resumed entry.\n\nIf you do not have access to the `team` scope you can only receive time entries for [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get).\nIf you want to fetch or modify time entries of other users, make sure your access token grants you access to the `team` scope and your user has the `administrator` role.",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Successfully resumed existing entry",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"time_entry": {
"allOf": [
{
"$ref": "#/components/schemas/entry"
}
],
"description": "The modified time entry. A `200` will occur if your organsiation's timesheet type is set to `hours`.\n When you are resuming a time entry for today, Keeping will modify that exact entry, and the `time_entry.id` will correspond\n with the path parameter `{entry_id}`. However, it could be that you are resuming an entry from an earlier date, which\n resumed a different entry that was created on the current day, therefore make sure to check `time_entry.id`."
},
"meta": {
"type": "object",
"properties": {
"created_additional_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where created as well next to the modified entry referenced in `time_entry`.\nThis can happen when an ongoing entry was stopped which wrapped it around an existing `break`."
},
"modified_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [
456789123
],
"description": "A list of time entries that where modified by the modified entry referenced in `time_entry`.\nThis can happen when another ongoing entry is stopped by this action."
},
"deleted_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where deleted by the modified entry referenced in `time_entry`.\nThis can happen when an ongoing `break` entry was stopped by this action."
}
}
}
}
},
"examples": {
"resume": {
"summary": "Resume an entry on the `hours` timesheet",
"value": {
"time_entry": {
"id": 456789123,
"user_id": 789456,
"date": "2022-04-05",
"purpose": "work",
"project_id": 56790,
"task_id": 34567,
"note": "Working on some e-mails",
"external_references": [
{
"id": "d69e192e3827b90e9d13e888317113e1",
"type": "generic_work_reference",
"name": "Send e-mail to venue",
"url": "https://planner.ellas-evenementen.nl/todos/123456789"
}
],
"start": "2022-04-05T14:27:15+02:00",
"end": null,
"hours": 1.5,
"ongoing": true,
"locked": false
},
"meta": {
"created_additional_time_entry_ids": [],
"modified_existing_time_entry_ids": [],
"deleted_existing_time_entry_ids": []
}
}
}
}
}
}
},
"201": {
"description": "Successfully created new ongoing entry",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"time_entry": {
"allOf": [
{
"$ref": "#/components/schemas/entry"
}
],
"description": "The newly created ongoing entry.\nPlease note that the `time_entry.id` will be different from the provided path parameter `{entry_id}`."
},
"meta": {
"type": "object",
"properties": {
"created_additional_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where created as well next to the created entry referenced in `time_entry`.\nThis can happen when you resume a `break` and this action splits entries of another purpose."
},
"modified_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [
456789123
],
"description": "A list of time entries that where modified by the created entry referenced in `time_entry`.\nThis can happen when another ongoing entry is stopped by this action."
},
"deleted_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where deleted by the created entry referenced in `time_entry`.\nThis can happen when an ongoing `break` entry was stopped by this action."
}
}
}
}
},
"examples": {
"resume": {
"summary": "Resume an entry on the `times` timesheet",
"value": {
"time_entry": {
"id": 789612541,
"user_id": 789456,
"date": "2022-04-05",
"purpose": "work",
"project_id": 56790,
"task_id": 34567,
"note": "Working on some e-mails",
"external_references": [
{
"id": "d69e192e3827b90e9d13e888317113e1",
"type": "generic_work_reference",
"name": "Send e-mail to venue",
"url": "https://planner.ellas-evenementen.nl/todos/123456789"
}
],
"start": "2022-04-05T14:27:15+02:00",
"end": null,
"hours": null,
"ongoing": true,
"locked": false
},
"meta": {
"created_additional_time_entry_ids": [],
"modified_existing_time_entry_ids": [
456789123
],
"deleted_existing_time_entry_ids": []
}
}
}
}
}
}
},
"403": {
"description": "You are not allowed to resume this time entry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "No entry found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/time-entries/{entry_id}/stop": {
"patch": {
"summary": "Stop an ongoing time entry",
"description": "Is a special variation of '[update time entry](#tag/entries/paths/~1{organisation_id}~1time-entries~1{entry_id}/patch)'.\nJust modifies the referenced time entry in such a way that it is stopped at the moment the request is received by the server.\n\nWhen your organisation's timesheet type is set to `times`, this corresponds to updating with the current time set in the `end` parameter.\n\nWhen your organisation's timesheet type is set to `hours`, this corresponds to updating the `hours` parameter and incrementing it with the time passed since the `start` of the ongoing entry. Therefor, you do not have to calculate the difference.\n \nPlease note that you can only stop an ongoing time entry.\n\nIf your organisation uses `breaks` and `times` you should take extra care interpreting the data of the resulting entry.\nRead the documentation of the [update request](#tag/entries/paths/~1{organisation_id}~1time-entries~1{entry_id}/patch) and the documentation of the response schema under `meta` for more information.",
"tags": [
"entries"
],
"security": [
{
"auth": [
"time"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"name": "entry_id",
"description": "The `id` of the entry you want to stop.\n\nIf you do not have access to the `team` scope you can only receive time entries for [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get).\nIf you want to fetch or modify time entries of other users, make sure your access token grants you access to the `team` scope and your user has the `administrator` role.",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Successfully stopped the ongoing entry",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"time_entry": {
"allOf": [
{
"$ref": "#/components/schemas/entry"
}
],
"description": "The modified time entry, of which the `time_entry.id` will correspond with the provided path parameter `{entry_id}`.\nPlease note that this is not always true when [resuming an entry](#tag/entries/paths/~1{organisation_id}~1time-entries~1{entry_id}~1resume/post)."
},
"meta": {
"type": "object",
"properties": {
"created_additional_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where created as well next to the stopped entry referenced in `time_entry`."
},
"modified_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [
456789123
],
"description": "A list of time entries that where modified by the stopped entry referenced in `time_entry`.\nThis can happen when you stop a `break`."
},
"deleted_existing_time_entry_ids": {
"type": "array",
"items": {
"type": "integer"
},
"example": [],
"description": "A list of time entries that where deleted by the stopped entry referenced in `time_entry`.\nThis can happen when you stop a `break`."
}
}
}
}
}
}
}
},
"403": {
"description": "You are not allowed to stop this time entry",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "No entry found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/report": {
"get": {
"summary": "Retrieve a report",
"description": "Retrieve an overview of all the hours worked that meet some specific criteria.\nFor example, you can:\n - Get all hours for the entire year grouped by users\n - Get the hours in a particular project within a particular week grouped by tasks\n - Get the hours a user worked on a specific task in the entire year, grouped by weeks",
"tags": [
"reports"
],
"security": [
{
"auth": [
"reporting"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"name": "from",
"in": "query",
"schema": {
"type": "string",
"format": "date"
},
"required": true,
"description": "The from date of the period the report should describe. This date is included in the period.\nMake sure its the same or earlier than the `to` date, and the difference in days is not greater than 366 days.\nPerform multiple requests if you want to fetch data for longer periods.\nThe format is: `Y-m-d`.\nRead the [section on dates](#section/General-usage/Dates-and-time-zones) for more information."
},
{
"name": "to",
"in": "query",
"schema": {
"type": "string",
"format": "date"
},
"required": true,
"description": "The to date of the period the report should describe. This date is included in the period.\nMake sure its the same or later than the `from` date, and the difference in days is not greater than 366 days.\nPerform multiple requests if you want to fetch data for longer periods.\nThe format is: `Y-m-d`.\nRead the [section on dates](#section/General-usage/Dates-and-time-zones) for more information."
},
{
"name": "row_type",
"in": "query",
"schema": {
"type": "string",
"enum": [
"project",
"task",
"client",
"user",
"external_reference",
"external_reference_type",
"day",
"week",
"month",
"quarter",
"year"
]
},
"required": true,
"description": "Select by which type of item the hours should be grouped.\nIf you want an item in the response to represent the hours worked on a specific project you should choose `project`, etc.\n\nYou can only choose `project` and `client` if `projects` are enabled.\nYou can only choose `task` if `tasks` are enabled.\nYou can only choose `external_reference` and `external_reference_types` if the organisation at least has subscription plan `plus_2019`."
},
{
"name": "user_ids",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"explode": true,
"description": "Filter the result to only include hours of time entries from **these** users.\nIf you want to retrieve the hours for all users, leave out this parameter.\nNot finding one of the users will result in a `404`.\n\nWhen your own user is a `team_member`, this parameter is considered **required** and **should** contain the single id of\nyour user: `?user_ids[]={your_user_id}`. Do not add additional user id's in this situation, otherwise you will receive a `403` response."
},
{
"name": "project_ids",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"explode": true,
"description": "Filter the result to only include hours of time entries with **these** projects.\nNot finding one of the projects will result in a `404`.\nIf you want to retrieve the hours for all projects, including for time entries that do not have a associated project, leave out this parameter.\nYou can only use this parameter if `projects` are enabled."
},
{
"name": "client_ids",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"explode": true,
"description": "Filter the result to only include hours of time entries for **these** clients.\nNot finding one of the clients will result in a `404`.\nIf you want to retrieve the hours for all clients, including for time entries that do not have a associated client, leave out this parameter.\nYou can only use this parameter if `projects` are enabled."
},
{
"name": "task_ids",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"explode": true,
"description": "Filter the result to only include hours of time entries with **these** tasks.\nNot finding one of the tasks will result in a `404`.\nIf you want to retrieve the hours for all tasks, including for time entries that do not have a associated task, leave out this parameter.\nYou can only use this parameter if `tasks` are enabled."
},
{
"name": "external_reference_ids",
"in": "query",
"schema": {
"type": "array",
"items": {
"pattern": "^[0-9a-f]{10,40}$",
"type": "string"
}
},
"explode": true,
"description": "Filter the result to only include hours of time entries with **these** external references.\nIf one of the external references is not found it will be ignored.\nIf you want to retrieve the hours for all external references, including for time entries that do not have a external references, leave out this parameter.\nYou can only choose this parameter if the organisation at least has subscription plan `plus_2019`."
},
{
"name": "external_reference_types",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "string",
"enum": [
"basecamp_1_todo",
"basecamp_2_todo",
"basecamp_3_todo",
"trello_card",
"asana_task",
"github_issue",
"jira_issue",
"todoist_todo",
"generic_work_reference",
"moneybird_invoice",
"exact_online_invoice",
"jortt_invoice",
"eboekhouden_invoice",
"twinfield_invoice",
"snelstart_invoice"
]
}
},
"explode": true,
"description": "Filter the result to only include hours of time entries with **these** external references types.\nIf one of the types is invalid we will return a `422`.\nFor example, if you only want to see hours associated with `github_issue`s or `jira_issue`s you can do so using this parameter.\n\nIf you want to retrieve the hours for all types, including for time entries that do not have a external references, leave out this parameter.\nYou can only choose this parameter if the organisation at least has subscription plan `plus_2019`."
}
],
"responses": {
"200": {
"description": "Successfully retrieved a report",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"report": {
"$ref": "#/components/schemas/report"
}
}
},
"examples": {
"projects": {
"summary": "A report of all projects",
"value": {
"report": {
"from": "2022-04-04",
"to": "2022-04-10",
"row_type": "project",
"rows": [
{
"id": "12346",
"description": "Branch Opening #fr1 – Ms. Francis",
"url": null,
"hours": 23.4,
"direct_hours": 19.2
},
{
"id": "78945",
"description": "General – Ella's Evenementenbureau",
"url": null,
"hours": 5.7,
"direct_hours": 0
}
],
"total_hours": 29.1,
"total_direct_hours": 19.2
}
}
}
}
}
}
},
"403": {
"description": "You are not allowed access to this report",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Entity referenced in parameter does not exist",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/report/time-entries": {
"get": {
"summary": "List a report's time entries",
"description": "Retrieve time entries that satisfy a specific predicate determined by its parameters.\nFor example, use this if you want to get the specific time entries of multiple users all at once. \n\nThis result is **paginated**, so you will need to send the server multiple requests if you want to\nretrieve a large dataset of specific time entries.\nPlease take our [usage limitations](#section/General-usage/Usage-limitations) and\nthe [usage etiquette](#section/General-usage/Usage-etiquette) into account when doing so. ",
"tags": [
"reports"
],
"security": [
{
"auth": [
"reporting"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"name": "from",
"in": "query",
"schema": {
"type": "string",
"format": "date"
},
"description": "The from date of the period the report should describe. This date is included in the period.\nMake sure its the same or earlier than the `to` date.\nThe format is: `Y-m-d`.\nRead the [section on dates](#section/General-usage/Dates-and-time-zones) for more information."
},
{
"name": "to",
"in": "query",
"schema": {
"type": "string",
"format": "date"
},
"description": "The to date of the period the report should describe. This date is included in the period.\nMake sure its the same or later than the `from` date. \nThe format is: `Y-m-d`.\nRead the [section on dates](#section/General-usage/Dates-and-time-zones) for more information."
},
{
"name": "user_ids",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"explode": true,
"description": "Filter the result to only include time entries from **these** users.\nIf you want to retrieve the time entries of all users, leave out this parameter.\nNot finding one of the users will result in a `404`.\n\nWhen your own user is a `team_member` this parameter is **required** and **should** contain the single id of\n your user: `?user_ids[]={your_user_id}`. Do not add additional user id's in this situation, otherwise you will receive a `403` response."
},
{
"name": "project_ids",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"explode": true,
"description": "Filter the result to only include time entries with **these** projects.\nNot finding one of the projects will result in a `404`.\nIf you want to retrieve the time entries of all projects, including the ones that do not have an associated project, leave out this parameter.\nYou can only use this parameter if `projects` are enabled."
},
{
"name": "client_ids",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"explode": true,
"description": "Filter the result to only include time entries for **these** clients.\nNot finding one of the clients will result in a `404`.\nIf you want to retrieve the time entries of all clients, including the ones that do not have an associated client, leave out this parameter.\nYou can only use this parameter if `projects` are enabled."
},
{
"name": "task_ids",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer"
}
},
"explode": true,
"description": "Filter the result to only include time entries with **these** tasks.\nNot finding one of the tasks will result in a `404`.\nIf you want to retrieve the time entries of all tasks, including the ones that do not have an associated task, leave out this parameter.\nYou can only use this parameter if `tasks` are enabled."
},
{
"name": "external_reference_ids",
"in": "query",
"schema": {
"type": "array",
"items": {
"pattern": "^[0-9a-f]{10,40}$",
"type": "string"
}
},
"explode": true,
"description": "Filter the result to only include time entries with **these** external references.\nIf you want to retrieve the time entries of all external references, including the ones that do not have external references, leave out this parameter.\nIf one of the external references is not found it will be ignored."
},
{
"name": "external_reference_types",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "string",
"enum": [
"basecamp_1_todo",
"basecamp_2_todo",
"basecamp_3_todo",
"trello_card",
"asana_task",
"github_issue",
"jira_issue",
"todoist_todo",
"generic_work_reference",
"moneybird_invoice",
"exact_online_invoice",
"jortt_invoice",
"eboekhouden_invoice",
"twinfield_invoice",
"snelstart_invoice"
]
}
},
"explode": true,
"description": "Filter the result to only include hours of time entries with **these** external references types.\nA single invalid type will result in a `422`.\nIf you want to retrieve the time entries of all types, including the ones that do not have external references, leave out this parameter.\nFor example, if you only want to see time entries associated to `github_issue` or `jira_issue` you can do so using this parameter."
},
{
"$ref": "#/components/parameters/page"
},
{
"$ref": "#/components/parameters/per_page"
}
],
"responses": {
"200": {
"description": "Successfully retrieved time entries",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"time_entries": {
"type": "array",
"items": {
"$ref": "#/components/schemas/entry"
}
},
"meta": {
"type": "object",
"properties": {
"total_hours": {
"type": "float",
"minimum": 0,
"example": 1.5,
"description": "Describes the total number of hours for all time entries across all pages."
}
},
"allOf": [
{
"$ref": "#/components/schemas/pagination_data"
}
]
}
}
}
}
}
},
"403": {
"description": "You are not allowed access to these time entries",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Entity referenced in parameter does not exist",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/projects": {
"get": {
"summary": "List projects",
"description": "In order to manage all your organisation's projects your user should be an `administrator`. Otherwise this will\nonly return projects where your own user is participating in, or has ever created a time entry with.\n\nUse to fetch all the valid project options when you are considering creating a new time entry. \nIn this case you should provide a `user_id`, e.g. use the URL parameters\n`?user_id={the_user_id_you_want_to_create_an_entry_for}&state[]=active` to find all the available valid projects.\nThis is the primary reason projects are also accessible **without** the `project_management` scope.\n\nThe response is **paginated**, so you will need to send the server multiple requests if you want to\nretrieve all projects when there are more than `100`.",
"tags": [
"projects"
],
"security": [
{
"auth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"name": "state",
"description": "Use this parameter to access archived projects. By default Keeping will only return active projects.\nYou can access both archived and active projects at once by providing them as an array, e.g. `?state[]=active&state[]=archived`.",
"in": "query",
"schema": {
"type": "array",
"default": [
"active"
],
"items": {
"type": "string",
"enum": [
"active",
"archived"
]
}
},
"explode": true
},
{
"name": "task_id",
"description": "Can only be used when `tasks` are enabled. Filter the projects down to the ones that have assignments to this specific task.\nWhen not provided Keeping will return the projects of for all tasks, including the ones without task assignments.",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "user_id",
"description": "Filter the projects down to the ones that are participated in by this specific user.\n\nWhen not provided Keeping will return the projects for all users, including the ones without participations.\n\nIf your own user is a `team_member` or does not have access to the `team` scope\nthis parameter can only contain the `id` of [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get).",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "client_id",
"description": "Filter the projects to only include the ones for this specific client.\nWhen not provided Keeping will return the projects for all clients, including the ones without a client.",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"$ref": "#/components/parameters/page"
},
{
"$ref": "#/components/parameters/per_page"
}
],
"responses": {
"200": {
"description": "Successfully fetched projects",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"projects": {
"type": "array",
"items": {
"$ref": "#/components/schemas/project"
}
},
"meta": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/pagination_data"
}
]
}
}
}
}
}
},
"403": {
"description": "You are not allowed to fetch these projects",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Entity referenced in parameters does not exist",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"post": {
"summary": "Create a project",
"description": "",
"tags": [
"projects"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/project_create_request"
}
}
}
},
"responses": {
"201": {
"description": "Project successfully created",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"project": {
"$ref": "#/components/schemas/project"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to create this project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Entity provided in the request body not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"422": {
"description": "Validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/projects/{project_id}": {
"get": {
"summary": "Retrieve a project",
"description": "In order to manage all your organisation's projects your user should be an administrator.\nOtherwise this will only return the project when your own user is participating in it, or has ever created a time entry with it.\n\nUsable **without** the `project_management` scope.",
"tags": [
"projects"
],
"security": [
{
"auth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"200": {
"description": "Successfully retrieved project",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"project": {
"$ref": "#/components/schemas/project"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to access this project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"patch": {
"summary": "Update a project",
"description": "",
"tags": [
"projects"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/project_edit_request"
}
}
}
},
"responses": {
"200": {
"description": "Successfully updated this project",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"project": {
"$ref": "#/components/schemas/project"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to update this project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Project not found (or entity provided in the request body not found)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"422": {
"description": "Validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"delete": {
"summary": "Delete a project",
"description": "Permanently delete a project. Please note that you **cannot** delete a project that has time entries associated with it. Trying to do so will result in a `403`. You can only archive these projects.",
"tags": [
"projects"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"204": {
"description": "Successfully deleted the project"
},
"403": {
"description": "You are not allowed to delete this project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/projects/{project_id}/archive": {
"patch": {
"summary": "Archive a project",
"description": "Sets the project's state to `archived`. You can only archive a project when it isn't used in any ongoing entries at the moment.",
"tags": [
"projects"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"200": {
"description": "Successfully archived this project",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"project": {
"$ref": "#/components/schemas/project"
}
}
},
"examples": {
"archived": {
"value": {
"project": {
"id": 56790,
"client": {
"id": 123456,
"name": "Ms. Francis",
"code": null,
"state": "active"
},
"name": "Branch Opening",
"code": "fr1",
"direct": "is_direct_through_task_assignments",
"task_assignments": [
{
"task_id": 34567,
"direct": true
}
],
"participations": [
{
"user_id": 789456
}
],
"state": "archived"
}
}
}
}
}
}
},
"403": {
"description": "You are not allowed to archive this project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/projects/{project_id}/restore": {
"patch": {
"summary": "Restore a project",
"description": "Sets the project's state to `active`.",
"tags": [
"projects"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"200": {
"description": "Successfully restored this project",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"project": {
"$ref": "#/components/schemas/project"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to restore this project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/tasks": {
"get": {
"summary": "List tasks",
"description": "This result is **paginated**, so you will need to send the server multiple requests if you want to\nretrieve all tasks when there are more than `100`.",
"tags": [
"tasks"
],
"security": [
{
"auth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"$ref": "#/components/parameters/page"
},
{
"$ref": "#/components/parameters/per_page"
},
{
"name": "state",
"description": "Use this parameter to access archived tasks. By default Keeping will only return active tasks.\nYou can access both archived and active tasks at once by providing them as an array, e.g. `?state[]=active&state[]=archived`.",
"in": "query",
"schema": {
"type": "array",
"default": [
"active"
],
"items": {
"type": "string",
"enum": [
"active",
"archived"
]
}
},
"explode": true
},
{
"name": "project_id",
"description": "Can only be used if `projects` are enabled. Filter the tasks down to the ones that have assignments to this specific project.\nWhen not provided Keeping will return the tasks for all projects, including the ones without assignments to projects.",
"in": "query",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "Successfully fetched tasks",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/task"
}
},
"meta": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/pagination_data"
}
]
}
}
}
}
}
},
"403": {
"description": "You are not allowed to fetch tasks",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Project referenced in parameter does not exist",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"post": {
"summary": "Create a task",
"description": "",
"tags": [
"tasks"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"name": "assign_to_active_projects",
"description": "When `true` Keeping will immediately assign the new task to all active projects after creation.\nIn the future the created task will also be assigned to new projects by default.\n\nBy default Keeping assumes this parameter is set to `true`, even if not provided.\nYou need to explicitly pass it `false` to prevent the immediate and future assignment to the projects from happening.",
"in": "query",
"schema": {
"type": "boolean",
"default": true
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/task_create_request"
}
}
}
},
"responses": {
"201": {
"description": "Task successfully created",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"task": {
"$ref": "#/components/schemas/task"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to create this task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"422": {
"description": "Validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/tasks/{task_id}": {
"get": {
"summary": "Retrieve a task",
"description": "",
"tags": [
"tasks"
],
"security": [
{
"auth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"200": {
"description": "Successfully retrieved task",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"task": {
"$ref": "#/components/schemas/task"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to access this task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"patch": {
"summary": "Update a task",
"description": "",
"tags": [
"tasks"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/task_edit_request"
}
}
}
},
"responses": {
"200": {
"description": "Successfully updated this task",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"task": {
"$ref": "#/components/schemas/task"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to update this task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"422": {
"description": "Validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"delete": {
"summary": "Delete a task",
"description": "Permanently delete a task. Please note that you **cannot** delete a task that has time entries associated with it. Trying to do so will result in a `403`. You can only archive these tasks.",
"tags": [
"tasks"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"204": {
"description": "Successfully deleted the task"
},
"403": {
"description": "You are not allowed to delete this task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/tasks/{task_id}/archive": {
"patch": {
"summary": "Archive a task",
"description": "Sets the task's state to `archived`. You can only archive a task when it isn't used in any ongoing entries at the moment.",
"tags": [
"tasks"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"200": {
"description": "Successfully archived this task",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"task": {
"$ref": "#/components/schemas/task"
}
}
},
"examples": {
"archived": {
"value": {
"task": {
"id": 34567,
"name": "Marketing",
"code": null,
"direct": true,
"state": "archived"
}
}
}
}
}
}
},
"403": {
"description": "You are not allowed to archive this task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/tasks/{task_id}/restore": {
"patch": {
"summary": "Restore a task",
"description": "Sets the task's state to `active`.",
"tags": [
"tasks"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"200": {
"description": "Successfully restored the task",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"task": {
"$ref": "#/components/schemas/task"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to restore this task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/clients": {
"get": {
"summary": "List clients",
"description": "List the clients in the organisation. Requires the `project_management` scope and for you to be an `administrator`.\nHowever, you can still receive client data through '[list projects](#tag/projects/paths/~1{organisation_id}~1projects/get)' when you do not meet these requirements.\n\nThis result is **paginated**, so you will need to send the server multiple requests if you want to\nretrieve all clients when there are more than `100`.",
"tags": [
"clients"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
},
{
"$ref": "#/components/parameters/page"
},
{
"$ref": "#/components/parameters/per_page"
},
{
"name": "state",
"description": "Use this parameter to access inactive clients. By default Keeping will only return active clients.\nA client is considered `active` when it has active projects linked to it, or when it has no projects linked to it at all.\nOne is considered `inactive` when it only has archived projects linked to it.\nYou can access both active and inactive clients at once by providing them as an array, e.g. `?state[]=active&state[]=inactive`.",
"in": "query",
"schema": {
"type": "array",
"default": [
"active"
],
"items": {
"type": "string",
"enum": [
"active",
"inactive"
]
}
},
"explode": true
}
],
"responses": {
"200": {
"description": "Successfully fetched clients",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"clients": {
"type": "array",
"items": {
"$ref": "#/components/schemas/client"
}
},
"meta": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/pagination_data"
}
]
}
}
}
}
}
},
"403": {
"description": "You are not allowed to fetch clients",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"post": {
"summary": "Create a client",
"description": "",
"tags": [
"clients"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/client_create_request"
}
}
}
},
"responses": {
"201": {
"description": "Client successfully created",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"client": {
"$ref": "#/components/schemas/client"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to create this client",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"422": {
"description": "Validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
},
"/{organisation_id}/clients/{client_id}": {
"get": {
"summary": "Retrieve a client",
"description": "",
"tags": [
"clients"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"200": {
"description": "Successfully retrieved client",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"client": {
"$ref": "#/components/schemas/client"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to access this client",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Client not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"patch": {
"summary": "Update a client",
"description": "",
"tags": [
"clients"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/client_edit_request"
}
}
}
},
"responses": {
"200": {
"description": "Successfully updated this client",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"client": {
"$ref": "#/components/schemas/client"
}
}
}
}
}
},
"403": {
"description": "You are not allowed to update this client",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Client not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"422": {
"description": "Validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
},
"delete": {
"summary": "Delete a client",
"description": "Permanently delete a client. Please note that you **cannot** delete a client that has projects associated with it. Trying to do so will result in a `403`.",
"tags": [
"clients"
],
"security": [
{
"auth": [
"project_management"
]
}
],
"parameters": [
{
"$ref": "#/components/parameters/organisation_id"
}
],
"responses": {
"204": {
"description": "Successfully deleted the client"
},
"403": {
"description": "You are not allowed to delete this client",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
},
"404": {
"description": "Client not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/error_response"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"client": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Primary key, use this identifier to uniquely identify a specific client.",
"example": 123456
},
"name": {
"type": "string",
"example": "Ms. Francis",
"description": "The unique name of the client."
},
"code": {
"type": "string",
"x-nullable": true,
"nullable": true,
"example": null,
"description": "A unique code that you can use internally. It can be used throughout Keeping to identify specific entities.\nIt is unique not only among clients but also tasks, projects and users."
},
"state": {
"type": "string",
"enum": [
"active",
"inactive"
],
"description": "A created client starts out as an `active` client, but can be change to `inactive` when it has only archived projects associated with it."
}
}
},
"client_create_request": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"maxLength": 191,
"description": "The name of the client. This **should** be unique among all clients within your organisation's Keeping environment.",
"example": "Ms. Francis"
},
"code": {
"type": "string",
"maxLength": 30,
"pattern": "^[0-9a-zA-Z\\.\\-\\/_]{1,30}$",
"x-nullable": true,
"nullable": true,
"default": null,
"example": null,
"description": "A unique code that you can use internally. It can be used throughout Keeping to identify specific entities.\nIt **should** be unique within your entire organisation, not only among clients but also projects, tasks and users."
}
}
},
"client_edit_request": {
"type": "object",
"properties": {
"name": {
"type": "string",
"maxLength": 191,
"description": "The name of the client. This **should** be unique among all clients within your organisation's Keeping environment.",
"example": "Ms. Francis"
},
"code": {
"type": "string",
"maxLength": 30,
"pattern": "^[0-9a-zA-Z\\.\\-\\/_]{1,30}$",
"x-nullable": true,
"nullable": true,
"example": null,
"description": "A unique code that you can use internally. It can be used throughout Keeping to identify specific entities.\nIt **should** be unique within your entire organisation, not only among clients but also projects, tasks and users."
}
}
},
"entry": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Primary key, use this identifier to uniquely identify a specific time entry.",
"example": 456789123
},
"user_id": {
"type": "integer",
"example": 789456,
"description": "Identifier of the user whose time entry this is."
},
"date": {
"type": "string",
"format": "date",
"example": "2022-04-05",
"description": "Date of the time entry. Time entries are always associated with specific date irrespective of a specific time zone.\nRead the [section on dates](#section/General-usage/Dates-and-time-zones) for more information."
},
"purpose": {
"type": "string",
"enum": [
"work",
"break"
],
"default": "work",
"description": "This describes the purpose of time entry. Regular time entries are `work` time entries.\n\nTime entries with the purpose `break` are a special type of time entry that do not count towards the total\namount of worked time, and are reported on separately. You can only create or modify these entries when `breaks` are \nenabled for your organisation."
},
"project_id": {
"type": "integer",
"nullable": true,
"x-nullable": true,
"example": 56790,
"description": "The identifier of the project this entry is describing time worked for.\nCan be `null` when `projects` are/were disabled for the entire organisation. Will always be `null` for time entries with the `break` purpose."
},
"task_id": {
"type": "integer",
"nullable": true,
"x-nullable": true,
"example": 34567,
"description": "The identifier of the task this entry is describing time worked on.\nCan be `null` when `tasks` are/were disabled for the entire organisation. Will always be `null` for time entries with the `break` purpose."
},
"note": {
"type": "string",
"maxLength": 10000,
"description": "An optional note describing what the user did during the time this entry represents. Can be `null`.",
"example": "Working on some e-mails",
"nullable": true,
"x-nullable": true
},
"external_references": {
"type": "array",
"description": "Used to refer to data in external application. A single time entry can be linked to multiple external references. \nThey will show up in Keeping's timesheet, including a URL which the user can directly use to go to the external data.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[0-9a-f]{10,40}$",
"maxLength": 40,
"minLength": 10,
"example": "d69e192e3827b90e9d13e888317113e1",
"description": "Unique identifier of a specific data point in an external application."
},
"type": {
"type": "string",
"enum": [
"basecamp_1_todo",
"basecamp_2_todo",
"basecamp_3_todo",
"trello_card",
"asana_task",
"github_issue",
"jira_issue",
"todoist_todo",
"generic_work_reference",
"moneybird_invoice",
"exact_online_invoice",
"jortt_invoice",
"eboekhouden_invoice",
"twinfield_invoice",
"snelstart_invoice"
],
"example": "generic_work_reference",
"description": "Describes the application this reference is linking to. When an entry is linked to an invoice or payslip it will automatically be `locked`."
},
"name": {
"type": "string",
"example": "Send e-mail to venue",
"description": "Name to describe the data point from the external application."
},
"url": {
"type": "string",
"maxLength": 2048,
"nullable": true,
"x-nullable": true,
"example": "https://planner.ellas-evenementen.nl/todos/123456789",
"description": "URL linking to the data point from the external application."
}
}
}
},
"start": {
"type": "string",
"format": "time",
"nullable": true,
"x-nullable": true,
"example": "2022-04-05T13:45:10+02:00",
"description": "Describes the start time of the time entry including the date and the time zone.\nIts date mostly corresponds to the `date` property of the entry, but can be different in specific cases when the time zone was adjusted afterwards. \nThe start can be `null` when the organisation's timesheet type is/was set to `hours`.\nRead the [section on dates](#section/General-usage/Dates-and-time-zones) for more information on the date time format.\n\nWhen the organisation's timesheet type is set to `hours` **and** the entry is `ongoing`, this value will **not** be `null`, but represent the last time the entry was set to ongoing.\nIn this case you should **ONLY** use this value for calculating the up-to-date hours value.\n\nWhen the organisation's timesheet type is set to `time` you can display the start time to your users.\n\nWhen a time entry is ongoing you should calculate the difference between the current time and the time in `start` and add it to the `hours` to get at the up-to-date hours the time entry is tracking.\nMake sure to [account for potential differences between the server time and your device time](#section/General-usage/Server-time) if you want your calculated time to match the time in Keeping's UI exactly. "
},
"end": {
"type": "string",
"format": "time",
"nullable": true,
"x-nullable": true,
"example": "2022-04-05T15:15:10+02:00",
"description": "Describes the end time of the time entry including the date and the time zone.\nIts date mostly corresponds to the `date` property of the entry, but can be different in specific cases when the time zone was adjusted afterwards. \nRead the [section on dates](#section/General-usage/Dates-and-time-zones) for more information on the date time format.\n\nWhen the organisation's timesheet type is/was set to `hours` this value will be `null`.\n\nWhen the organisation's timesheet type is set to `time` you can display the start time to your users. If the entry is `ongoing` in this case, the end will be `null` as well."
},
"hours": {
"type": "number",
"format": "float",
"minimum": 0,
"example": 1.5,
"nullable": true,
"x-nullable": true,
"description": "This is the time entry's confirmed value expressed in hours.\nIt does **not** include the ongoing time since it was last started.\nThis you as a developer should calculate at the client-side. \n\nMinutes are expressed in the decimal points where each minute is roughly `0.0167` hours.\n\nIn order to find the total time from a collection of entries you should sum the `hours` value of each entry.\nIn general entries with the `break` purpose should not be counted when calculating the total amount of hours.\n\nThe value does not account for a currently ongoing entry.\nWhen the organisation's timesheet type is set to `times` this merely reflects the amount of time between `start` and `end`, and will be `null` in the case of an ongoing entry.\nWhen the timesheet type is set to `hours` this value can be `null` for a newly created ongoing entry, or not `null` if an entry was resumed after it already accumulated some time."
},
"ongoing": {
"type": "boolean",
"default": false,
"description": "Indicates the time entry is currently tracking, i.e. the stop watch is running. A user can only have a single running entry at a time.\n\nWhen a time entry is ongoing you should calculate the difference between the current time and the time in `start` and add it to the `hours` to get at the up-to-date hours the time entry is tracking.\nMake sure to [account for potential differences between the server time and your device time](#section/General-usage/Server-time) if you want your calculated time to match the time in Keeping's UI exactly. "
},
"locked": {
"type": "boolean",
"default": false,
"description": "Indicates the time entry is locked and cannot be modified or deleted. A time entry can be locked when its project or\ntask has been archived, when it has an invoice or payslip associated with it or when its purpose is `break` but breaks\nare disabled. "
}
}
},
"entry_create_request": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/entry_edit_request"
}
],
"properties": {
"user_id": {
"type": "integer",
"example": 789456,
"description": "The `id` of the user you want to create a time entry for. Defaults to [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get) when the property is not provided on the request.\n\nIf you do not have access to the `team` scope you can only create time entries for [your own user](#tag/users/paths/~1{organisation_id}~1users~1me/get). If you want to create time entries for other users, make sure your access token grants you access to the `team` scope and your user has the `administrator` role.\n\nOnce a time entry is created you cannot change its `user_id`.\n\nWhen the organisation's timesheet is `hours`, Keeping will use the `user_id` as one of the distinguishing properties when it looks if an entry needs to be modified instead of created."
},
"date": {
"type": "string",
"format": "date",
"default": "2022-04-05 (today)",
"example": "2022-04-05",
"description": "The day on which you want to create a time entry, irrespective of the time zone.\nThere is a single exception, your organisation's time zone does matter for the default value when the property is not provided on the request.\nIn which case, the value will default to the current day in your organisation's time zone.\n\nThe format is: `Y-m-d`. Read the [section on dates](#section/General-usage/Dates-and-time-zones) for more information.\n\nOnce a time entry is created you cannot change its `date`.\n\nWhen the organisation's timesheet is `hours`, Keeping will use the `date` as one of the distinguishing properties when it looks if an entry needs to be modified instead of created."
},
"purpose": {
"type": "string",
"enum": [
"work",
"break"
],
"default": "work",
"description": "The purpose describes what type of time entry you want to create. Regular time entries are `work` time entries.\nWhen your organisation's timesheet type is set to `times`, these types of entries can overlap with each other but will be marked with an alert in\nKeeping's interface.\n\nTime entries of the type `break` can only be created when `breaks` are enabled for your entire organisation. Breaks are\na special type of time entry that do not count towards the total amount of worked time, and are reported on separately.\n\nOnce a time entry is created you cannot change its `purpose`.\n\nWhen the organisation's timesheet type is set to `hours`, Keeping will use the `purpose` as one of the distinguishing properties when\nit looks if an entry needs to be modified instead of created.\n\n### Overlap prevention for breaks\nWhen your organisation's timesheet type is set to `times`, breaks can exhibit **special behaviour** which prevents them\nfrom overlapping with time entries that have another purpose.\n\nYou can create breaks that would overlap with other time entries.\nHowever, Keeping will then automatically adjust the existing time entries so that the break is created according to your specification without any overlap.\nWhen other entries get modified or deleted because of a new break,\nKeeping will notify you using `meta.modified_existing_time_entry_ids` or `meta.deleted_existing_time_entry_ids` respectively.\n\nBreaks can overlap with other breaks but will be marked with an alert in Keeping's interface.\n\nWhen you create a time entry with a different purpose and there already is a break that would overlap with the new entry,\nKeeping will adjust the `start` or `end` of your new entry as to prevent overlap with the break.\nKeeping will also split your new entry in multiple entries if necessary,\nin which case you receive one in `time_entry` and references to the others in `meta.created_additional_time_entry_ids`.\nIf the creation of a time entry is impossible due to the full overlap with a break,\nor if the creation would require you to adjust a locked entry, you will receive a `403`."
},
"project_id": {
"type": "integer",
"example": 56790,
"description": "When your organisation has `projects` enabled, you will be **required** to provide the `project_id` of an **active** project of which the user (described by `user_id`) is a **participant**, when creating a time entry with the purpose `work`.\nIf `projects` are disabled for your organisation you can leave out this property.\n\nParameter can be left out of the request when creating a `break` time entry.\n\nWhen the organisation's timesheet type is set to `hours`, Keeping will use the `project_id` as one of the distinguishing properties\nwhen it looks if an entry needs to be modified instead of created."
},
"task_id": {
"type": "integer",
"example": 34567,
"description": "When your organisation has `tasks` enabled, you will be **required** to provide a `task_id` when creating a time entry with the purpose `work`. In case `projects` are enabled as well, the task should be **active** and currently\n**associated** with the project you specified using `project_id`. When `projects` are disabled you can enter the `task_id` of any **active** task.\nIf `tasks` are disabled for your organisation you can leave out this property.\n\nParameter can be left out of the request when creating a `break` time entry.\n\nWhen the organisation's timesheet type is set to `hours`, Keeping will use the `task_id` as one of the distinguishing properties\nwhen it looks if an entry needs to be modified instead of created."
},
"note": {
"type": "string",
"example": "Working on some e-mails",
"default": null,
"nullable": true,
"x-nullable": true,
"maxLength": 10000,
"description": "An **optional** note you can add to your time entry, you can use it to describe the entry itself.\n\nWhen the organisation's timesheet type is set to `hours`, Keeping will use the `note` as one of the distinguishing properties when\nit looks if an entry needs to be modified instead of created. Keeping will make sure to trim whitespace from the note when doing this."
},
"external_references": {
"type": "array",
"maximum": 10,
"description": "If you want to link your time entry to some external data you can use this property to store the link. A single time entry can be linked to multiple external references.\nThey will show up in Keeping's timesheet, including a URL which the user can directly use to go to the external data.\n\n![Example of external reference](/images/developer/external-reference.png \"Example of external reference\")\n\nKeeping supports links to various project management applications.\nYou yourself cannot create links to these applications but you can create an external reference of type `generic_work_reference` for your own application.\nGeneric work references are displayed within Keeping with a link icon, and can link to any application.\n\nWhen the organisation's timesheet type is set to `hours`, Keeping will use the `external_references` as one of the distinguishing properties\nwhen it looks if an entry needs to be modified instead of created.",
"items": {
"type": "object",
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[0-9a-f]{10,40}$",
"maxLength": 40,
"minLength": 10,
"example": "d69e192e3827b90e9d13e888317113e1",
"description": "Provide a hash in hex format you yourself can use to uniquely identify the external reference.\nYou could use a hashing algorithm such as [MD5](https://nl.wikipedia.org/wiki/MD5) or [SHA-1](https://en.wikipedia.org/wiki/SHA-1).\nThe fact that these algorithms are not secure enough for modern password hashing does not matter in our case.\n\nIf you attach an external reference with the same identifier to another entry Keeping will see them as a link to the exact same object.\n\nFor example if your application is called \"My Cool App\" and you want to link to a to-do with your internal identifier `1234`\nyou could use the resulting hash from `md5(\"my_cool_app.todo.1234\")` as your identifier, i.e. `d69e192e3827b90e9d13e888317113e1`."
},
"type": {
"type": "string",
"enum": [
"generic_work_reference"
],
"description": "Using the API you will only be able to create external references of the type `generic_work_reference`. Other types exist\nbut are reserved for Keeping's internals."
},
"name": {
"type": "string",
"maxLength": 191,
"example": "Send e-mail to venue",
"description": "Provide a name for the external item to display within Keeping's UI."
},
"url": {
"type": "string",
"nullable": true,
"x-nullable": true,
"maxLength": 2048,
"example": "https://planner.ellas-evenementen.nl/todos/123456789",
"description": "Provide a valid URL to the item within your application. An invalid URL will result in a `422`."
}
}
}
},
"start": {
"type": "string",
"example": "13:45",
"default": "12:04 (current time)",
"description": "Can only be used if the organisation's timesheet type is set to `times`, it will be ignored otherwise.\n\nDefaults to the current time (in your organisation's time zone) when not provided.\n\nThis property accepts the time in almost all formats and handles them gracefully.\nBoth 24-hour and AM/PM formats are accepted, so [PHP date formats](https://www.php.net/manual/en/function.date.php):\n`g:ia` (e.g. `1:15pm`) and `G:i` (e.g. `13:15`) are both accepted.\nYou should **not** provide the full date, only a time is accepted by this property.\n\nIf this property is left out of the response Keeping will assume the current time, even for entries on earlier dates.\nThe `start` property is **required** for entries on future dates."
},
"end": {
"type": "string",
"example": "15:15",
"default": null,
"nullable": true,
"x-nullable": true,
"description": "Can only be used if the organisation's timesheet type is set to `times`, it will be ignored otherwise.\n\nThis property accepts the time in almost all formats and handles them gracefully.\nBoth 24-hour and AM/PM formats are accepted, so [PHP date formats](https://www.php.net/manual/en/function.date.php):\n`g:ia` (e.g. `1:15pm`) and `G:i` (e.g. `13:15`) are both accepted.\nYou should **not** provide the full date, only a time is accepted by this property.\n\nIf this property is left out of the response and `start` is in the past you will create an ongoing time entry.\nThe `end` property is **required** for entries on future dates.\n\nA user can only have a single running entry, so when you create an ongoing entry while there already was one, Keeping will\nstop the previous ongoing entry.\nThis will be reflected in the `meta.modified_existing_time_entry_ids`.\n\nKeeping assumes the `end` always to be exactly the same or later than the `start` time.\nThis means it is possible to have entries end the next day. For example, you could have an entry that starts at `23:00` in the\n evening and end at `4:00` in the morning. In this case the entry still belongs to the `date` the entry was started on."
},
"hours": {
"type": "number",
"format": "float",
"minimum": 0,
"maximum": 1000,
"example": 1.5,
"description": "Can only be used if the organisation's timesheet type is set to `hours`, it will be ignored otherwise.\n\nIf this property is left out of the response you will create an ongoing time entry.\nOtherwise you will create a finished time entry with an exact amount of hours.\n\nA user only has a single running entry, so when you create an ongoing entry while there already was one, Keeping will\nstop the previous ongoing entry.\nThis will be reflected in the `meta.modified_existing_time_entry_ids`."
}
}
},
"entry_edit_request": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/entry_edit_request"
}
],
"properties": {
"project_id": {
"type": "integer",
"example": 56790,
"description": "When your organisation has `projects` enabled, you can provide the `project_id` of an **active** project of which the entrie's user is a **participant**.\nIf `projects` are disabled for your organisation you can leave out this property.\n\n`break` time entries cannot have an assigned project.\n\nWhen the organisation's timesheet type is set to `hours`, Keeping will use the `project_id` as one of the distinguishing properties\nwhen it looks if an entry needs to be modified instead of created."
},
"task_id": {
"type": "integer",
"example": 34567,
"description": "When your organisation has `tasks` enabled, you can provide a `task_id`. In case `projects` are enabled as well, the task should be **active** and currently\n**associated** with the project you specified using `project_id`. When `projects` are disabled, you can enter the `task_id` of any **active** task.\nIf `tasks` are disabled for your organisation you can leave out this property.\n\n`break` time entries cannot have an assigned task.\n\nWhen the organisation's timesheet type is set to `hours`, Keeping will use the `task_id` as one of the distinguishing properties\nwhen it looks if an entry needs to be modified instead of created."
},
"note": {
"type": "string",
"example": "Working on some e-mails",
"nullable": true,
"x-nullable": true,
"maxLength": 10000,
"description": "An **optional** note you can add to your time entry, you can use it to describe the entry itself.\n\nWhen the organisation's timesheet type is set to `hours`, Keeping will use the `note` as one of the distinguishing properties when\nit looks if an entry needs to be modified instead of created. Keeping will make sure to trim whitespace from the note when doing this."
},
"external_references": {
"type": "array",
"maximum": 10,
"description": "If you want to link you time entry to some external data you can use this property to store the link. A single time entry can be linked to multiple external references.\nThey will show up in Keeping's timesheet, including a URL which the user can directly use to go to the external data.\n\n![Example of external reference](/images/developer/external-reference.png \"Example of external reference\")\n\nKeeping supports links to various project management applications. You yourself cannot create links to these applications but you can create a `generic_work_reference` for your own application.\nYou can however keep links of other external reference types that already exist. You do so by submitting the exact same external reference data that you received when retrieving the time entry on modification.\nWhen you leave out existing external references from the array they will be deleted.\n\nIf you do not include `external_references` in your request as a property they will not be modified. To delete all of them provide an emtpy array.\n\nWhen the organisation's timesheet type is set to `hours`, Keeping will use the `external_references` as one of the distinguishing properties\nwhen it looks if an entry needs to be modified instead of created.",
"items": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[0-9a-f]{10,40}$",
"maxLength": 40,
"minLength": 10,
"example": "d69e192e3827b90e9d13e888317113e1",
"description": "Provide a hash in hex format you yourself can use to uniquely identify the external reference.\nYou could use a hashing algorithm such as [MD5](https://nl.wikipedia.org/wiki/MD5).\nThe fact that these algorithms are not secure enough for modern password hashing does not matter in our case.\n\nIf you attach an external reference with the same identifier to another entry Keeping will consider them as a link to the exact same object.\n\nFor example if your application is called \"My Cool App\" and you want to link to a to-do with your internal identifier `1234`\nyou could use the resulting hash from `md5(\"my_cool_app.todo.1234\")` as your identifier, i.e. `d69e192e3827b90e9d13e888317113e1`."
},
"type": {
"type": "string",
"enum": [
"generic_work_reference"
],
"description": "Using the API you will only be able to create external references of the type `generic_work_reference`. Other types exist\nbut are reserved for Keeping's internals.\n\nYou **cannot** change a external reference's type after it has been created."
},
"name": {
"type": "string",
"maxLength": 191,
"example": "Send e-mail to venue",
"description": "Provide a name for the external item to display within Keeping's UI.\nIs **required** for new external references."
},
"url": {
"type": "string",
"nullable": true,
"x-nullable": true,
"maxLength": 2048,
"example": "https://planner.ellas-evenementen.nl/todos/123456789",
"description": "Provide a valid URL to the item within your application. An invalid URL will result in a `422`."
}
}
}
},
"start": {
"type": "string",
"example": "13:45",
"description": "Can only be used if the organisation's timesheet type is set to `times`, it will be ignored otherwise.\n\nYou can only enter start times that are in the past if you want to modify an ongoing entry.\n\nThis property accepts the time in almost all formats and handles them gracefully.\nBoth 24-hour and AM/PM formats are accepted, so [PHP date formats](https://www.php.net/manual/en/function.date.php):\n`g:ia` (e.g. `1:15pm`) and `G:i` (e.g. `13:15`) are both accepted.\nYou should **not** provide the full date, only a time is accepted by this property."
},
"end": {
"type": "string",
"example": "15:15",
"description": "Can only be used if the organisation's timesheet type is set to `times`, it will be ignored otherwise.\n\nCan be left out for ongoing entries. If you want to stop an ongoing entry on a specific time, you should submit a valid `end` time and the entry will be stopped.\n\nThis property accepts the time in almost all formats and handles them gracefully.\nBoth 24-hour and AM/PM formats are accepted, so [PHP date formats](https://www.php.net/manual/en/function.date.php):\n`g:ia` (e.g. `1:15pm`) and `G:i` (e.g. `13:15`) are both accepted.\nYou should **not** provide the full date, only a time is accepted by this property.\n\nKeeping assumes the `end` always to be exactly the same or later than the `start` time.\nThis means it is possible to have entries end the next day. For example, you could have an entry that starts at `23:00` in the\n evening and end at `4:00` in the morning. In this case the entry still belongs to the `date` the entry was started on."
},
"hours": {
"type": "number",
"format": "float",
"minimum": 0,
"maximum": 1000,
"example": 1.5,
"description": "Can only be used if the organisation's timesheet type is set to `hours`, it will be ignored otherwise.\n\nIf the entry is ongoing and you submit `hours` it will automatically be stopped.\nWhen a time entry is ongoing you should calculate the difference between the current time and the time in `start` and add it to the `hours` to get at the up-to-date hours the time entry is tracking, if you want to offset the hours including the ongoing time.\nYou can also use '[stop a time entry](#tag/entries/paths/~1{organisation_id}~1time-entries~1{entry_id}~1stop/patch)' to stop ongoing entries and increment the `hours` correctly without you having to make a calculation."
}
}
},
"error": {
"type": "object",
"properties": {
"message": {
"description": "Description of the error that can be used to display a message to the user. If possible, this message will be localized in accordance with the `Accept-Language` header value. Data from this error description is not suitable for use in error handling logic other than displaying a message to the user, and should always be considered subject to change.",
"type": "string"
}
}
},
"error_response": {
"type": "object",
"properties": {
"error": {
"description": "The error object describing what went wrong.\n\nPlease do **not** assume the existence of a JSON encoded error message, it could very well be that our load balancer returns a HTML document or something else entirely. So when handling error responses, first check the status code and only if present look at this JSON encoded error message for more information.",
"allOf": [
{
"$ref": "#/components/schemas/error"
}
]
}
}
},
"oauth_access_token": {
"type": "object",
"properties": {
"token_type": {
"type": "string",
"enum": [
"Bearer"
]
},
"expires_in": {
"description": "Describes the amount of seconds the access token will be valid. Make sure to calculate the exact moment of expiration yourself to prevent unnecessary requests to the web service.",
"type": "int",
"default": 86400
},
"access_token": {
"description": "The access token you can now send along with further requests in the `Authorization` header to authenticate yourself. The header value should be: `Bearer {access_token}`. This token conforms to the [JWT format](https://jwt.io/).",
"type": "string",
"format": "password",
"example": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjdmZGRkYjIwYjBjZTA3NmY5OWE2NTE2MDMwZTY1NjRiMDA5MDZhYTFhN2ZjNDM4NjdkNzdmZWVkM2IwZDE4NjBmZmQxNTcxMTU3OTk3ZWJhIn0.eyJhdWQiOiIxIiwianRpIjoiN2ZkZGRiMjBiMGNlMDc2Zjk5YTY1MTYwMzBlNjU2NGIwMDkwNmFhMWE3ZmM0Mzg2N2Q3N2ZlZWQzYjBkMTg2MGZmZDE1NzExNTc5OTdlYmEiLCJpYXQiOjE1NzU0NzMxMTMsIm5iZiI6MTU3NTQ3MzExMywiZXhwIjoxNTc2MDc3OTEzLCJzdWIiOiIxIiwic2NvcGVzIjpbInRpbWUiLCJ0ZWFtIiwicmVwb3J0aW5nIiwicHJvamVjdF9tYW5hZ2VtZW50Il19.lxKL-QT550zkP5iP0R3RQRsRUdBofmbPLVuqfUdncXVXSeBl1R7d3aCa"
},
"refresh_token": {
"description": "When the access token has expired you should request a new one using this refresh token.",
"type": "string",
"format": "password",
"example": "b9ad932293dabe3bb9133225b5a71667f9fc94bd32888fd35c90585ea8bf64ba8a9473661fef9f396f995b9e8fa858c9f272cc92286aac6f5db1fb8c5af89faab8d08b26a1c213b9a36795cdba23fb13331dc71576ac680df43115ddff3cfc9c8729a3a59026f9a480ade6e409bd08a090a098ccd3768bacc7126697a46131d210ffada72fc23b2a8d13c719d0e3c31fd65551483e999cadb1d6535b248fc2542ee73d11e80e4da8a5e0680bbaa6164d0f816289a2dad938f62372844550d630334c8f182f7e64a6"
}
}
},
"oauth_error": {
"type": "object",
"properties": {
"error": {
"description": "String representing the type of error, which can be used for programmatically handling specific errors.",
"type": "string",
"enum": [
"invalid_request",
"invalid_client",
"invalid_grant",
"unauthorized_client",
"unsupported_grant_type",
"invalid_scope"
],
"example": "invalid_request"
},
"error_description": {
"description": "Human readable description of the error meant for the developer. This string will **not** be localized.",
"type": "string",
"example": "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed."
},
"hint": {
"description": "A hint that could point the developer in the direction of solving the problem. This string will **not** be localized.",
"type": "string",
"example": "Authorisation code has expired"
}
}
},
"oauth_request": {
"type": "object",
"required": [
"grant_type",
"client_id",
"client_secret",
"redirect_uri"
],
"discriminator": {
"propertyName": "grant_type",
"mapping": {
"authorization_code": "#/components/schemas/oauth_request_auth_code",
"refresh_token": "#/components/schemas/oauth_request_refresh_token"
}
},
"properties": {
"grant_type": {
"description": "The grant type you wish to use to request a new access token.",
"type": "string",
"enum": [
"authorization_code",
"refresh_token"
]
},
"client_id": {
"description": "Your OAuth application's client id provided by Keeping to you as a developer.",
"type": "string",
"example": "12345"
},
"client_secret": {
"description": "Your OAuth application's client secret provided by Keeping to you as a developer. Make sure to keep this value secret.",
"type": "string",
"format": "password",
"example": "mQ1dY4hcAibu9MnKsstBRb5rxKiugQpceYK1FNNl"
},
"redirect_uri": {
"description": "The exact redirect URL you registered with Keeping for your OAuth application. This could be a URL with the `https` scheme or a custom scheme.",
"type": "string",
"example": "https://ella-evenementen.nl/keeping-redirect/"
}
}
},
"oauth_request_auth_code": {
"allOf": [
{
"$ref": "#/components/schemas/oauth_request"
},
{
"type": "object",
"required": [
"code"
],
"properties": {
"code": {
"description": "The authorisation code you received as a URL parameter (`?code=…`) when the user was directed back to your application's `redirect_uri`.",
"type": "string",
"example": "b9ad932293dabe3bb9133225b5a71667f…"
}
}
}
]
},
"oauth_request_refresh_token": {
"allOf": [
{
"$ref": "#/components/schemas/oauth_request"
},
{
"type": "object",
"required": [
"refresh_token"
],
"properties": {
"refresh_token": {
"description": "The refresh token you received with your last access token.",
"type": "string",
"example": "b9ad932293dabe3bb9133225b5a71667f…"
},
"scope": {
"description": "Specify the scopes you wish to use for your new access token. You can only request fewer scopes than you did on your last access token request. When using multiple chain them using the `+` character, e.g. `time+team`. _Please note: do not use `%2B` to chain scopes._",
"type": "string",
"allowReserved": true,
"example": "time",
"enum": [
"time",
"reporting",
"team",
"project_management"
]
}
}
}
]
},
"organisation": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Primary key, use this identifier to uniquely identify a specific organisation.",
"example": 12345
},
"name": {
"type": "string",
"description": "The organisation's name you setup in the Keeping settings.",
"example": "Ella's Evenementenbureau"
},
"url": {
"type": "string",
"description": "The URL you can use to access the organisation's Keeping environment.",
"example": "https://ellas-evenementen.keeping.nl"
},
"current_plan": {
"type": "string",
"description": "The current Keeping plan the entire organisation is subscribed to. Read [the section on authorisation](#section/Authentication/Authorisation) for more information.",
"example": "plus_2019",
"enum": [
"free_2019",
"free_2018",
"standard_2018",
"plus_2019",
"enterprise_2019"
]
},
"features": {
"type": "object",
"description": "A collection of feature toggles on how the organisation uses Keeping. Read [the section on authorisation](#section/Authentication/Authorisation) for more information.",
"properties": {
"timesheet": {
"type": "string",
"description": "Describes the type of timesheet that is currently enabled for your organisation. The `times` timesheet has time entries with a start and end so you have can have an exact trail of everyone work on any specific day. The `hours` timesheet allows you to enter hours per day, irrespective of the exact moment the work took place.",
"enum": [
"times",
"hours"
],
"default": "times"
},
"projects": {
"type": "boolean",
"description": "Tells you whether or not **projects and clients** are enabled for the organisation. An administrator is allowed to disable them for the entire organisation. In the case projects are disabled you will not be required to specify a project when creating an entry, you neither can request projects or group reports on projects.",
"default": true
},
"tasks": {
"type": "boolean",
"description": "Tells you whether or not tasks are enabled for the organisation. An administrator is allowed to disable them for the entire organisation. In the case tasks are disabled you will not be required to specify a task when creating an entry, you neither can request tasks or group reports on tasks.",
"default": true
},
"breaks": {
"type": "boolean",
"description": "Tells you whether or not breaks are enabled for the organisation. Breaks can only be enabled for organisations that are on the `plus_2019` or `enterprise_2019` plan. Breaks allow you to create time entries with the purpose: `break`. Time entries with this `purpose` do not count towards the total worked hours, and Keeping will make sure they never overlap with time entries with another purpose.",
"default": false
}
}
},
"time_zone": {
"type": "string",
"description": "The current time zone used by default in the entire organisation. The string format is that of the [tz database](https://en.wikipedia.org/wiki/Tz_database). Please [read the section on dates and time zones](#section/General-usage/Dates-and-time-zones) for more information.",
"default": "Europe/Amsterdam"
},
"currency": {
"type": "string",
"description": "The currency used by default in the entire organisation. This is the currency that should be used for monetary values in the reports. The value contains the [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) code of the currency.",
"default": "EUR"
}
}
},
"pagination_data": {
"type": "object",
"properties": {
"total": {
"type": "integer",
"minimum": 0,
"example": 1,
"description": "Describes the total number of entities across all pages."
},
"per_page": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"example": 25,
"description": "The number of entities per page defined by the `per_page` URL parameter."
},
"current_page": {
"type": "integer",
"minimum": 1,
"example": 1,
"description": "The current page defined by the `page` URL parameter."
},
"last_page": {
"type": "integer",
"minimum": 1,
"x-nullable": true,
"nullable": true,
"example": 1,
"description": "The last available page. `null` when there are no entities found and therefore no last page exists."
}
}
},
"project": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Primary key, use this identifier to uniquely identify a specific project.",
"example": 56790
},
"client": {
"allOf": [
{
"$ref": "#/components/schemas/client"
}
],
"description": "The single client associated with this project. Within the Keeping UI projects are usually grouped by their client. A client **can** be `null` when the project is not specific to a client, this could be true for internal projects.",
"nullable": true,
"x-nullable": true
},
"name": {
"type": "string",
"example": "Branch Opening",
"description": "The unique name of the project."
},
"code": {
"type": "string",
"x-nullable": true,
"nullable": true,
"example": "fr1",
"description": "A unique code that you can use internally. It can be used throughout Keeping to identify specific entities.\nIt is unique not only among projects but also tasks, clients and users."
},
"direct": {
"type": "string",
"enum": [
"entire_project_is_indirect",
"is_direct_through_task_assignments",
"entire_project_is_direct"
],
"example": "is_direct_through_task_assignments",
"x-nullable": true,
"nullable": true,
"description": "Describes if time entries with this project will be counted as direct.\nThis property will be `null` when your organisation does not have at least subscription plan `plus_2019`."
},
"task_assignments": {
"type": "array",
"x-nullable": true,
"nullable": true,
"items": {
"type": "object",
"properties": {
"task_id": {
"type": "integer",
"example": 34567,
"description": "The primary key of the assigned task. Use the '[fetch task](#tag/tasks/paths/~1{organisation_id}~1tasks~1{task_id}/get)' to retrieve more information."
},
"direct": {
"type": "boolean",
"x-nullable": true,
"nullable": true,
"example": true,
"description": "If your project's `direct` property equals `is_direct_through_task_assignments` this will indicate whether the task-project combination should be counted as direct. This property will be `null` when your organisation does not have at least subscription plan `plus_2019`."
}
}
},
"description": "Will be `null` when tasks are disabled.\nWhen tasks are enabled, this property lists all tasks associated to this project including both `active` and `archived` tasks.\nPlease note, when both tasks and projects are enabled, you can only create time entries for an active assigned task with an active project."
},
"participations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"user_id": {
"type": "integer",
"example": 789456,
"description": "The primary key of the participating user. Use the '[fetch user](#tag/users/paths/~1{organisation_id}~1users~1{userId}/get)' to retrieve more information."
}
}
},
"description": "This property lists of all users participating in this project, this includes both `active` and users with other states. Please note, that only participating users can use this project to create time entries."
},
"state": {
"type": "string",
"enum": [
"active",
"archived"
],
"description": "The current state of the project. Only `active` projects can be used to create new time entries. In order to archive a project use '[archive](#tag/projects/paths/~1{organisation_id}~1projects~1{project_id}~1archive/patch)', in order to reactivate an `archived` project use '[restore](#tag/projects/paths/~1{organisation_id}~1projects~1{project_id}~1restore/patch)'."
}
}
},
"project_create_request": {
"type": "object",
"required": [
"name"
],
"properties": {
"client_id": {
"type": "integer",
"nullable": true,
"x-nullable": true,
"default": null,
"example": 123456,
"description": "The `id` of the client you want this project to be associated with. You can also leave this property out or set it to `null` to indicate that it is not associated with any client, but rather an internal project."
},
"name": {
"type": "string",
"maxLength": 191,
"description": "The name of the project. This **should** be unique among all projects within your organisation's Keeping environment.",
"example": "Branch Opening"
},
"code": {
"type": "string",
"maxLength": 30,
"pattern": "^[0-9a-zA-Z\\.\\-\\/_]{1,30}$",
"x-nullable": true,
"nullable": true,
"default": null,
"example": "fr1",
"description": "A unique code that you can use internally. It can be used throughout Keeping to identify specific entities.\nIt **should** be unique within your entire organisation, not only among projects but also tasks, clients and users."
},
"direct": {
"type": "string",
"enum": [
"entire_project_is_indirect",
"is_direct_through_task_assignments",
"entire_project_is_direct"
],
"default": "entire_project_is_direct",
"example": "is_direct_through_task_assignments",
"description": "Describes when the hours of time entries that use this project are considered direct. You can only provide this property when your organisation has at least subscription plan: `plus_2019`."
},
"task_assignments": {
"type": "array",
"items": {
"type": "object",
"required": [
"task_id"
],
"properties": {
"task_id": {
"type": "integer",
"example": 34567,
"description": "The `id` of the task linked to this project. This `id` should reference an existing task in this organisation. You **cannot** use the same `task_id` multiple times in the `task_assignments`."
},
"direct": {
"type": "boolean",
"example": true,
"description": "Describes if a time entry using this project and task combination is considered direct. You can only provide this property when the projects direct property is set to `is_direct_through_task_assignments` and when your organisation has at least subscription plan: `plus_2019`."
}
}
},
"description": "Describes the tasks that can be used in conjunction with this project for creating new time entries.\n\nYou can only use this property when `tasks` are enabled.\nWhen you leave out this property Keeping will automatically assign tasks to the project that are setup to be assigned to all new projects by default."
},
"participations": {
"type": "array",
"items": {
"type": "object",
"required": [
"user_id"
],
"properties": {
"user_id": {
"type": "integer",
"example": 34567,
"description": "The `id` of the user participating in this project. This `id` should reference an existing user in this organisation. You **cannot** use the same `user_id` multiple times in the `participations`."
}
}
},
"description": "Describes the users that can use this project when creating new time entries.\n\nWhen you leave out this property Keeping will automatically populate the project with participants that are setup to join all new projects by default."
}
}
},
"project_edit_request": {
"type": "object",
"properties": {
"client_id": {
"type": "integer",
"nullable": true,
"x-nullable": true,
"description": "The `id` of the client you want this project to be associated with. You can also leave this property out or set it to `null` to indicate that it is not associated with any client, but rather an internal project."
},
"name": {
"type": "string",
"maxLength": 191,
"description": "The name of the project. This **should** be unique among all projects within your organisation's Keeping environment.",
"example": "Branch Opening"
},
"code": {
"type": "string",
"maxLength": 30,
"pattern": "^[0-9a-zA-Z\\.\\-\\/_]{1,30}$",
"x-nullable": true,
"nullable": true,
"example": null,
"description": "A unique code that you can use internally. It can be used throughout Keeping to identify specific entities.\nIt **should** be unique within your entire organisation, not only among projects but also tasks, clients and users."
},
"direct": {
"type": "string",
"enum": [
"entire_project_is_indirect",
"is_direct_through_task_assignments",
"entire_project_is_direct"
],
"example": "is_direct_through_task_assignments",
"description": "Describes when the hours of time entries that use this project are considered direct. You can only provide this property when your organisation has at least subscription plan: `plus_2019`."
},
"task_assignments": {
"type": "array",
"required": [
"task_id"
],
"items": {
"type": "object",
"properties": {
"task_id": {
"type": "integer",
"example": 34567,
"description": "The `id` of the task linked to this project. This `id` should reference an existing task in this organisation. You **cannot** use the same `task_id` multiple times in the `task_assignments`."
},
"direct": {
"type": "boolean",
"example": true,
"description": "Describes if a time entry using this project and task combination is considered direct. You can only provide this property when the projects direct property is set to `is_direct_through_task_assignments` and when your organisation has at least subscription plan: `plus_2019`."
}
}
},
"description": "Describes the tasks that can be used in conjunction with this project for creating new time entries.\n\nMake sure to submit **ALL** the task assignments you want the project to have after the update, or simple do not include the property to make no changes.\nWhen you leave out a single task assignment Keeping will remove it from the project.\nTherefor, nobody will be able to create new time entries using this task and project combination anymore.\n\nYou can only use this property when `tasks` are enabled."
},
"participations": {
"type": "array",
"required": [
"user_id"
],
"items": {
"type": "object",
"properties": {
"user_id": {
"type": "integer",
"example": 34567,
"description": "The `id` of the user participating in this project. This `id` should reference an existing user in this organisation. You **cannot** use the same `user_id` multiple times in the `participations`."
}
}
},
"description": "Describes the users that can use this project when creating new time entries.\n\nMake sure to submit **ALL** the participations you want the project to have after the update, or simple do not include the property to make no changes.\nWhen you leave out a single participation Keeping will remove it from the project.\nTherefore, its user will not be able to create new time entries for this project anymore."
}
}
},
"report": {
"type": "object",
"properties": {
"from": {
"type": "string",
"format": "date",
"description": "The start day for the period this report is concerning, irrespective of a specific time zone.\nThe response will include time entries with a `date` from this day onward.\n\nWhen `from` is equal to `to` the report only concerns a single day. The difference between `from` and `to` will not be greater than 366 days.\n\nRead the [section on dates](#section/General-usage/Dates-and-time-zones) for more information."
},
"to": {
"type": "string",
"format": "date",
"description": "The end day for the period this report is concerning, irrespective of a specific time zone.\nThe response will include time entries with a `date` up to and including this day.\n\nWhen `from` is equal to `to` the report only concerns a single day. The difference between `from` and `to` will not be greater than 366 days.\n\nRead the [section on dates](#section/General-usage/Dates-and-time-zones) for more information."
},
"row_type": {
"type": "string",
"enum": [
"project",
"task",
"client",
"user",
"external_reference",
"external_reference_type",
"day",
"week",
"month",
"quarter",
"year"
],
"description": "A report will contain `rows` which represent the time worked on a specific row type.\nEach of your report's rows will be representing some period or entity from the Keeping API depending on the row type selected.\n\nFor example, when you select `project` each row will correspond to a project, and its `id` should be seen as a `project_id`.\nThe `hours` on the row will be the total hours worked on the project in the report's period and constrained to the request parameters."
},
"rows": {
"type": "array",
"description": "These report rows represent some period or entity from the Keeping API depending on the `row_type` of this report.",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"nullable": true,
"x-nullable": true,
"description": "When the report's `row_type` is set to `project` this id should be seen as a `project_id`, and similarly for other entities such as `task`, `client` and `user` and `external_reference`.\n\nIn the case that the report's `row_type` is set to a period (e.g. `month`) this id will be of the format: `Y-m-d..Y-m-d` describing the period, or `Y-m-d` if the period is just comprised of a single day.\n\nIn the case of an `row_type` of `external_reference_type` the id corresponds to a single `external_reference_type`, e.g. `basecamp_3_todo`."
},
"description": {
"type": "string",
"description": "A description of the report row. In some cases it will be dependant on the `Accept-Language` header.\n\nFor example, when the report's `row_type` is set to `project` it will describe the project with it's name, code and client."
},
"url": {
"type": "string",
"nullable": true,
"x-nullable": true,
"description": "The url of the `external_reference`. Is `null` for other row types."
},
"hours": {
"type": "number",
"format": "float",
"minimum": 0,
"description": "The total hours worked on the item in the report's period and constrained to the request parameters. Where the row is described ty the `row_type`.\n\nHours worked do **not** include the hours of the time entries with the purpose `break`. \n\nFor example, when the `row_type` is set to `user` this value will represent the total worked hours by a single user in the report's period and constrained to the request parameters."
},
"direct_hours": {
"type": "number",
"format": "float",
"nullable": true,
"x-nullable": true,
"minimum": 0,
"description": "Only contains the direct hours for this row. Whether an hours is counted as direct or not depends on the projects or tasks associated with the underlying time entries. \nUsing `direct_hours`, you can calculate the indirect hours by subtracting the direct hours from the total amount of `hours` of the row.\n\nWill be `null` when your organisation does not have at least the subscription plan `plus_2019`."
}
}
}
},
"total_hours": {
"type": "number",
"format": "float",
"minimum": 0,
"description": "The total hours worked on the report's period and constrained to the request parameters.\n\nWill equal the summation of the `hours` of all the `rows` in this report.\n\nHours worked do **not** include the hours of the time entries with the purpose `break`. "
},
"total_direct_hours": {
"type": "number",
"format": "float",
"nullable": true,
"x-nullable": true,
"minimum": 0,
"description": "Only contains the direct hours for the entire report. Whether an hours is counted as direct or not depends on the projects or tasks associated with the underlying time entries. \nUsing `total_direct_hours`, you can calculate the total indirect hours by subtracting the direct hours from the `total_hours` of the entire report.\n\nWill equal the summation of the `direct_hours` of all the `rows` in this report.\n\nWill be `null` when your organisation does not have at least the subscription plan `plus_2019`."
}
}
},
"task": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Primary key, use this identifier to uniquely identify a specific task.",
"example": 34567
},
"name": {
"type": "string",
"example": "Marketing",
"description": "The unique name for the task."
},
"code": {
"type": "string",
"x-nullable": true,
"nullable": true,
"example": null,
"description": "A unique code that you can use internally. It can be used throughout Keeping to identify specific entities.\nIt is unique not only among tasks but also projects, clients and users."
},
"direct": {
"type": "boolean",
"example": true,
"x-nullable": true,
"nullable": true,
"description": "When `projects` are disabled, describes if time entries with this task will be counted as direct.\n\nWhen `projects` are enabled, this property describes whether assignments to this task will be assumed to be direct by default.\nThis is especially relevant when the project's direct property is set to `is_direct_through_task_assignments` and no `task_assignments` are provided on project creation.\n\nThis property is `null` if your organisation does not have at least subscription plan `plus_2019`."
},
"state": {
"type": "string",
"enum": [
"active",
"archived"
],
"description": "The current state of the task. Only `active` tasks can be used to create new time entries. In order to archive a task use '[archive](#tag/tasks/paths/~1{organisation_id}~1tasks~1{task_id}~1archive/patch)', in order to reactivate an `archived` task use '[restore](#tag/tasks/paths/~1{organisation_id}~1tasks~1{task_id}~1restore/patch)'."
}
}
},
"task_create_request": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"maxLength": 191,
"description": "The name of the task. This **should** be unique among all tasks within your organisation's Keeping environment.",
"example": "Marketing"
},
"code": {
"type": "string",
"maxLength": 30,
"pattern": "^[0-9a-zA-Z\\.\\-\\/_]{1,30}$",
"x-nullable": true,
"nullable": true,
"default": null,
"example": null,
"description": "A unique code that you can use internally. It can be used throughout Keeping to identify specific entities.\nIt **should** be unique within your entire organisation, not only among tasks but also projects, clients and users."
},
"direct": {
"type": "boolean",
"example": true,
"default": true,
"description": "When `projects` are enabled Keeping will assume new task assignments to be direct by default if this property is set to `true`.\nWhen `projects` are disabled Keeping will assume all time entries associated with this task to be direct if this property is set to `true`.\nYou can only provide this property when your organisation has at least subscription plan: `plus_2019`."
}
}
},
"task_edit_request": {
"type": "object",
"properties": {
"name": {
"type": "string",
"maxLength": 191,
"example": "Marketing",
"description": "The name of the task. This **should** be unique among all tasks within your organisation's Keeping environment."
},
"code": {
"type": "string",
"maxLength": 30,
"pattern": "^[0-9a-zA-Z\\.\\-\\/_]{1,30}$",
"x-nullable": true,
"nullable": true,
"example": null,
"description": "A unique code that you can use internally. It can be used throughout Keeping to identify specific entities.\nIt **should** be unique within your entire organisation, not only among tasks but also projects, clients and users."
},
"direct": {
"type": "boolean",
"example": true,
"default": true,
"description": "When `projects` are enabled Keeping will assume new task assignments to be direct by default if this property is set to `true`.\nWhen `projects` are disabled Keeping will assume all time entries associated with this task to be direct if this property is set to `true`.\nYou can only provide this property when your organisation has at least subscription plan: `plus_2019`."
}
}
},
"user": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"example": 789456,
"description": "Primary key, use this identifier to uniquely identify a specific user."
},
"first_name": {
"type": "string",
"nullable": true,
"x-nullable": true,
"example": "Ella",
"description": "The user's first name, or first names."
},
"surname": {
"type": "string",
"nullable": true,
"x-nullable": true,
"example": "van Doorn",
"description": "The user's surname (i.e. last name) including a possible prefix.\nKeeping has special logic to deal with the most common Dutch surname prefixes, and order users correctly by their last name.\nSo there is no need to attach surname prefixes at the end of the like: `Doorn, van`, you can just use `van Doorn`."
},
"code": {
"type": "string",
"x-nullable": true,
"nullable": true,
"example": null,
"description": "A unique code that you can use internally. It can be used throughout Keeping to identify specific entities.\nIt is unique not only among users but also projects, clients and tasks."
},
"role": {
"type": "string",
"enum": [
"team_member",
"administrator"
],
"example": "administrator",
"description": "The role that indicates this users privileges. In general users with the role `team_member` can create time entries \nfor themselves and view reports on all their own time entries. Users with the role `administrator` can create time\nentries for everyone, view broad organisational reports, and manage projects, clients and tasks. More information\ncan be found in [the authorisation section](#section/Authentication/Authorisation)."
},
"state": {
"type": "string",
"enum": [
"needs_invite",
"invited",
"active",
"inactive",
"blocked",
"decoupled"
],
"example": "active",
"description": "Describes the current state of the `user`. This state does not impair other users from manipulating the time entries of this user.\nA `blocked` user can be considered archived and can't access Keeping. A `decoupled` user has left the organisation themselves and cannot access Keeping anymore, they should receive a new invite.\nA user that has the state `needs_invite` should first be invited before they can access Keeping.\nAn `invited` user has received an invite but still needs to accept it.\nOnly `active` and `inactive` users can actually use Keeping themselves.\nUsers that are `inactive` have not accessed Keeping for at least 50 days."
}
}
}
},
"parameters": {
"organisation_id": {
"name": "organisation_id",
"description": "The `id` of the organisation you want to use in this action. See [the section on organisations](#tag/organisations) for more information.",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
},
"page": {
"name": "page",
"description": "Since this request is paginated you can provide the page number you want to request, if you want to see beyond page `1`. Please consult `meta.last_page` to see the maximum page number.",
"in": "query",
"schema": {
"type": "integer",
"minimum": 1,
"default": 1
}
},
"per_page": {
"name": "per_page",
"description": "If you want to see more results on a single page you can increase the amount of items shown per page.",
"in": "query",
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 25
}
}
},
"securitySchemes": {
"auth": {
"type": "oauth2",
"description": "Below follows a schematic description of the authentication flows referenced by API URLs that require authentication.",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://developer.keeping.nl/oauth/authorize",
"tokenUrl": "https://internal-api.keeping.nl/oauth/token",
"refreshUrl": "https://internal-api.keeping.nl/oauth/token",
"scopes": {
"time": "Access and manage time entries.",
"reporting": "Access reports.",
"team": "Access data of the entire team, not only data of the authorizer him/herself.",
"project_management": "Manage projects, tasks and clients."
}
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment