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.