Time Entries API
All examples use:
https://app.keito.ai/api/v2
Add Authorization: Bearer <kto_...> and Keito-Account-Id: <company_id> to every API key request.
Create a Time Entry
POST /api/v2/time_entries
Creates a time entry. To start a running timer, set is_running to true.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
project_id |
string | Yes | Project ID |
task_id |
string | Yes | Task ID |
spent_date |
string | Yes | Date of work (YYYY-MM-DD) |
hours |
number | No | Duration in decimal hours for completed entries |
is_running |
boolean | No | Create a running timer |
replace_running |
boolean | No | When true with is_running, stop any currently running timer before creating this one. When false or omitted, conflicting running-timer creates return 409 Conflict. |
started_time |
string | No | Start time (HH:mm) in the workspace timezone |
ended_time |
string | No | End time (HH:mm) in the workspace timezone for completed entries |
notes |
string | No | Description of work |
billable |
boolean | No | Override billable status |
source |
string | No | web, cli, api, agent, calendar, or desktop |
metadata |
object | No | JSON object, max 4KB |
started_time and ended_time are HH:mm workspace-time strings, not ISO timestamps. When is_running is true, omit started_time for a current server-side start. Past timer starts require the Past timer starts company setting; future start times are rejected.
Only one timer can be active for a user. By default, creating a running timer is strict: if another timer is already running, the API returns 409 Conflict with error: "running_timer_conflict" and the current running_entry. Pass replace_running: true to intentionally switch timers in one request.
Running Timer Conflict
{
"error": "running_timer_conflict",
"error_description": "A timer is already running.",
"message": "A timer is already running.",
"running_entry": {
"id": "running_time_entry_id"
},
"running_entry_count": 1
}
Example Request
curl -X POST https://app.keito.ai/api/v2/time_entries \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id" \
-H "Content-Type: application/json" \
-d '{
"project_id": "project_id_here",
"task_id": "task_id_here",
"spent_date": "2026-05-05",
"hours": 1.5,
"notes": "Refactored authentication module",
"source": "api",
"metadata": {
"session_id": "550e8400-e29b-41d4-a716-446655440000"
}
}'
For a running timer, send is_running: true and omit hours. Send replace_running: true only when your integration should intentionally switch away from the currently running timer.
List Time Entries
GET /api/v2/time_entries
Returns a paginated list of time entries.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page |
number | Page number |
per_page |
number | Results per page |
source |
string | Filter by source |
project_id |
string | Filter by project |
task_id |
string | Filter by task |
user_id |
string | Filter by user |
client_id |
string | Filter by client |
from |
string | Start date, inclusive |
to |
string | End date, inclusive |
is_billed |
boolean | Filter billed status |
is_running |
boolean | Filter running timers |
updated_since |
string | ISO timestamp lower bound |
Example Request
curl "https://app.keito.ai/api/v2/time_entries?source=cli&from=2026-05-01&to=2026-05-31" \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id"
Example Response Shape
{
"time_entries": [],
"per_page": 100,
"total_pages": 0,
"total_entries": 0,
"page": 1,
"links": {
"first": "/api/v2/time_entries?page=1&per_page=100",
"next": null,
"previous": null,
"last": "/api/v2/time_entries?page=1&per_page=100"
}
}
Update a Time Entry
PATCH /api/v2/time_entries/:id
Updates editable fields on a time entry. source is immutable after creation.
curl -X PATCH https://app.keito.ai/api/v2/time_entries/time_entry_id \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id" \
-H "Content-Type: application/json" \
-d '{
"hours": 1.5,
"notes": "Completed OAuth implementation"
}'
Stop a Running Timer
PATCH /api/v2/time_entries/:id/stop
Stops a running time entry and calculates elapsed duration server-side. Empty request bodies are accepted. If notes is supplied and the entry is editable, it replaces the entry notes. Do not send hours, ended_time, or is_running:false to stop a live timer; use this endpoint.
curl -X PATCH https://app.keito.ai/api/v2/time_entries/time_entry_id/stop \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id" \
-H "Content-Type: application/json" \
-d '{"notes": "Finished implementation"}'
Restart a Time Entry
PATCH /api/v2/time_entries/:id/restart
Restarts a stopped time entry as the active timer. Pass replace_running: true to intentionally stop any currently running timer first.
curl -X PATCH https://app.keito.ai/api/v2/time_entries/time_entry_id/restart \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id" \
-H "Content-Type: application/json" \
-d '{"replace_running": true}'
If another timer is running and replace_running is not supplied, the API returns 409 Conflict with running_entry.
Delete a Time Entry
DELETE /api/v2/time_entries/:id
Deletes a time entry. Approved or locked entries cannot be deleted.
curl -X DELETE https://app.keito.ai/api/v2/time_entries/time_entry_id \
-H "Authorization: Bearer kto_xxxxx" \
-H "Keito-Account-Id: your_company_id"
Returns HTTP 204 No Content on success.