HourdiniDocs
Power tools

REST API

Drive Hourdini from your own scripts and integrations.

Hourdini has an HTTP API at /api/v1/* that mirrors the web app. If you're building an integration, an internal dashboard, or a one-off script, you can use it directly.

If you're not a developer

You probably want the command line or an AI agent instead. The API is a developer-facing surface.

Authenticating

Generate a Personal Access Token at /cli/connect and send it as a bearer:

curl https://hourdini.app/api/v1/clients \
  -H "Authorization: Bearer hd_pat_live_••••"

The token is bound to one user and one organization. Every call operates within that organization.

Confirm the token works against GET /api/v1/me, the identity probe. It returns the authenticated user and the org bound to the PAT.

curl https://hourdini.app/api/v1/me \
  -H "Authorization: Bearer hd_pat_live_••••"

A taste

# List your clients -> { "clients": [...] }
curl https://hourdini.app/api/v1/clients \
  -H "Authorization: Bearer hd_pat_live_••••"

# Start a timer -> { "timer": {...} }
curl https://hourdini.app/api/v1/timer \
  -X PUT \
  -H "Authorization: Bearer hd_pat_live_••••" \
  -H "Content-Type: application/json" \
  -d '{"project_name": "acme dash", "description": "design review"}'

# Inspect the running timer
curl https://hourdini.app/api/v1/timer \
  -H "Authorization: Bearer hd_pat_live_••••"

# Stop it
curl https://hourdini.app/api/v1/timer \
  -X DELETE \
  -H "Authorization: Bearer hd_pat_live_••••"

# Summary for an explicit date range
curl 'https://hourdini.app/api/v1/time-entries/summary?from=2026-05-04T00:00:00Z&to=2026-05-11T00:00:00Z' \
  -H "Authorization: Bearer hd_pat_live_••••"

Sessions

A session lives under /api/v1/sessions. The lifecycle is open, beat transitions, end. See Sessions for the concept.

# Open a session. The first beat is `active`.
curl https://hourdini.app/api/v1/sessions \
  -X POST \
  -H "Authorization: Bearer hd_pat_live_••••" \
  -H "Content-Type: application/json" \
  -d '{"project_id": "...", "description": "ticket #42"}'

# Flip the open beat (active | agent | idle).
curl https://hourdini.app/api/v1/sessions/{id}/beat \
  -X POST \
  -H "Authorization: Bearer hd_pat_live_••••" \
  -H "Content-Type: application/json" \
  -d '{"kind": "agent"}'

# Bring the session back to the foreground (active).
curl https://hourdini.app/api/v1/sessions/{id}/focus \
  -X POST \
  -H "Authorization: Bearer hd_pat_live_••••" \
  -d '{}'

# End it. Response includes the materialised `time_entry_id` and
# `billable_seconds` (active + agent if the project has bill_agent_time on).
curl https://hourdini.app/api/v1/sessions/{id} \
  -X DELETE \
  -H "Authorization: Bearer hd_pat_live_••••"

Open sessions don't appear in /api/v1/time-entries. The end call is what writes the entry, with entry_type: "session" and the session's billable seconds as the duration.

Conventions

  • Successful responses are wrapped in a single top-level key, e.g. { "clients": [...] } or { "timer": {...} }. Pull the field you asked for and ignore the rest.
  • All timestamps are ISO 8601 UTC.
  • REST list and summary endpoints use explicit from / to timestamps. CLI and MCP tools additionally accept the time period vocabulary.

Money fields are JSON strings

Every monetary value, fields suffixed _minor, _amount, or _total, is serialized as a JSON string of integer minor units (cents) in the field's currency. This preserves bigint precision past JavaScript's Number.MAX_SAFE_INTEGER. A USD $165.00 arrives as "default_rate_minor": "16500". Parse with BigInt() before you do arithmetic. Format for display via Intl.NumberFormat.

This applies to summary endpoints too: GET /api/v1/time-entries/summary returns total_minor, unbilled_minor, and per-currency totals as strings.

Error envelope

Errors share one shape:

{
  "error": {
    "code": "entry_invoiced",
    "message": "This entry is on a sent invoice and cannot be edited.",
    "details": { "invoice_id": "inv_..." }
  }
}

code is stable, message is human-readable, details is optional and varies per code.

Common codes:

CodeWhen you see it
not_authenticatedMissing, wrong, or revoked PAT.
no_orgThe PAT's org has been removed from your account.
not_foundThe id you sent doesn't exist or isn't in your org.
disambiguateA name match found multiple candidates. The response lists them; call again with an explicit id.
entry_invoicedYou tried to edit or delete a time entry that's on a sent invoice.
invalid_periodThe period value isn't a token we know. See Time periods.

Endpoint reference

The full machine-readable spec is at /api/v1/openapi.json. It's an OpenAPI 3.1 document with every endpoint, parameter, and request body schema, served public and unauthenticated so you can read it before you have a token.

Drop it into any OpenAPI-aware tool (Postman, Insomnia, Bruno, oapi-codegen, openapi-typescript, Scalar, etc.) to generate typed clients or browse interactively.

The AI agent overview is also useful for understanding workflows. MCP tools are designed around agent-friendly inputs, so use the OpenAPI document as the source of truth for REST request and response shapes.

On this page