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/totimestamps. 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:
| Code | When you see it |
|---|---|
not_authenticated | Missing, wrong, or revoked PAT. |
no_org | The PAT's org has been removed from your account. |
not_found | The id you sent doesn't exist or isn't in your org. |
disambiguate | A name match found multiple candidates. The response lists them; call again with an explicit id. |
entry_invoiced | You tried to edit or delete a time entry that's on a sent invoice. |
invalid_period | The 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.