Horaka API (1.0.0)

Download OpenAPI specification:

REST API v1, trigger API and outgoing webhooks of Horaka.

Horaka is time tracking for freelancers who bill their clients. This API lets an ERP, an automation tool or a hardware button read and write the data of one workspace.

Authentication

Every endpoint except the specification itself needs a token, sent as Authorization: Bearer <token>. Tokens are created under Settings, API in the app (/app/einstellungen/api). The plain token is shown exactly once; the server only keeps a hash. A revoked or expired token answers 401.

  • Personal tokens belong to one account and act with its role. Team members only see and change their own time entries and never see rates or amounts; owners and admins see everything in the workspace.
  • Device tokens belong to a device (iOS Shortcut, NFC tag, Flic button, Stream Deck, anything else) of one account. They only carry the ability track, can be limited to certain projects and are counted against the device limit of the plan (Free 1, Standard 3, Premium unlimited).

Abilities and plans

Ability Allows Plan
read all GET endpoints Standard and Premium
write creating, changing and archiving clients, projects, activities, time entries Premium
track trigger endpoints /track/* and GET /timer Premium (device tokens: every plan within the device limit)
billing billing runs (owners and admins) Premium
webhooks webhook endpoints (owners and admins) Premium

The delta sync (/time-entries/changes) is part of Premium. A missing ability answers 403 with code insufficient_scope, a feature outside the plan answers 403 with code plan_required and the plan that includes it in meta.required_plan. A deactivated account answers account_deactivated, a suspended workspace workspace_suspended, a workspace whose deletion the owner has requested workspace_deletion_pending (until the request is revoked).

Rate limits

Per token and minute: read 600, write 120, track 60, billing 30 (webhooks counts as write). Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining; above the limit the answer is 429 with Retry-After in seconds.

Format

  • Answers are {"data": …, "meta": …, "links": …}; errors are {"errors": [{"status", "code", "title", "detail", "source", "meta"}]}. Codes are stable and English, title and detail follow Accept-Language (en or de, default English).
  • Times are ISO 8601 in UTC (2026-10-14T08:00:00Z). Times you send need an offset or Z. Calendar days (local_date, from, to) are YYYY-MM-DD in the time zone of the account.
  • Amounts are integers in minor units (cents) with the ISO currency next to them.
  • Rounding, breaks and amounts are calculated by the server when a time entry is saved and are read from stored values. A running timer has ended_at: null and no values yet.

Pagination

Lists use keyset pagination: page[size] (default 50, at most 200) and page[cursor] with meta.next_cursor of the previous page. meta.has_more says whether another page follows, links.next is the ready-made address. Lists accept updated_since (ISO 8601) for incremental reads.

Delta sync for an ERP

GET /time-entries/changes?cursor=<n> returns every time entry changed after cursor in the order of the change (sync_version), deletions included as tombstones with deleted_at set. Store meta.next_cursor and ask again with it; while meta.has_more is true, ask again at once. Without new changes the cursor stays the same and If-None-Match with the last ETag answers 304. Tombstones are kept for 90 days; a cursor older than the oldest kept tombstone answers 410 with the code cursor_expired: start again without a cursor and replace your copy.

Trigger API

POST /track/start, /track/stop, /track/toggle and /track/switch take {"project": <id or alias>, "activity": <id or name>, "note": "…"} and always answer with the current timers. If the account allows parallel timers (personal setting), several timers may run at once: start adds a timer (the same project and activity never runs twice). Otherwise a start ends the running timers at the same moment (meta.action is switched). stop without a project stops all running timers, switch stops all other running timers at the same moment. Send an Idempotency-Key header (any unique text, valid for 24 hours) to make a press count exactly once; the same key returns the stored answer, the same key with a different body answers 422. Without the header, a second press of the same account within two seconds is ignored (meta.debounced: true). Aliases are maintained under Settings, API.

Webhooks

Owners and admins register https endpoints (no private networks) under Settings, Webhooks or with POST /webhooks. Each delivery is a POST with a JSON body (see the webhook section) and the headers X-Horaka-Event, X-Horaka-Delivery-Id and X-Horaka-Signature: t=<unix time>,v1=<hex> where v1 is HMAC-SHA256 of <t>.<raw body> with the endpoint secret. Answer with any 2xx within 10 seconds. Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h and 12 h; after 20 failures in a row the endpoint is switched off and the owner gets an email. Deliveries can be replayed in the app.

Verify the signature against the raw body and reject timestamps older than five minutes:

$header = $_SERVER['HTTP_X_HORAKA_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', $parts['t'].'.'.$body, getenv('HORAKA_WEBHOOK_SECRET'));
$valid = isset($parts['t'], $parts['v1'])
    && abs(time() - (int) $parts['t']) <= 300
    && hash_equals($expected, $parts['v1']);
// Node.js (Express with express.raw({ type: 'application/json' }))
import crypto from 'node:crypto';

function verify(req, secret) {
  const parts = Object.fromEntries(
    String(req.get('X-Horaka-Signature') || '').split(',').map((p) => p.split('=')),
  );
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${req.body}`).digest('hex');
  return Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300
    && parts.v1?.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
import hashlib, hmac, time

def verify(header: str, body: bytes, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
    return abs(time.time() - int(parts["t"])) <= 300 and hmac.compare_digest(expected, parts["v1"])

Examples

Start the timer on project MUE-WEB with curl:

curl -X POST https://api.horaka.com/v1/track/start \
  -H "Authorization: Bearer $HORAKA_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"project": "MUE-WEB", "note": "Workshop"}'
  • iOS Shortcuts: action "Get Contents of URL", method POST, URL …/v1/track/toggle, headers Authorization: Bearer <device token> and Content-Type: application/json, request body JSON with the key project. Run it from Siri, the home screen or an NFC automation. For a spoken status ("hours today"), call GET …/api/v1/track/status?format=text and pass the result to "Speak Text". The app has a gallery with ready requests under Settings, Shortcuts.
  • Zapier: "Webhooks by Zapier", action "Custom Request" with the same URL, headers and body; as a trigger use "Catch Hook" and register its address as a webhook endpoint.
  • Make: module "HTTP, Make a request" with the token as header; module "Webhooks, Custom webhook" to receive events.
  • n8n: node "HTTP Request" with "Header Auth" (Authorization: Bearer …); node "Webhook" to receive events, then verify the signature in a "Code" node.

Versioning

The version is part of the path (/v1). Additive changes (new fields, new endpoints, new events) come without a new version, so ignore fields you do not know. Endpoints that will be replaced get the headers Deprecation and Sunset well in advance.

Specification

This document as JSON and YAML, without a token.

Specification as JSON

Responses

Response samples

Content type
application/json
{ }

Specification as YAML

Responses

Clients

Clients of the workspace (the customers you bill).

List clients

Team members only see clients with a project they may see, without rates and notes.

Authorizations:
bearerAuth
query Parameters
updated_since
string <date-time>

Only records changed at or after this time (ISO 8601 with offset).

page[size]
integer [ 1 .. 200 ]
Default: 50

Entries per page, 1 to 200.

page[cursor]
string

meta.next_cursor of the previous page.

archived
string
Enum: "true" "false" "1" "0"

Only archived (true) or only active (false) clients; both when missing.

search
string <= 100 characters

Part of the name.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {
    }
}

Create a client

Owners and admins. Ability write.

Authorizations:
bearerAuth
Request Body schema: application/json
required
name
string <= 191 characters
color
string (ProjectColor)
Enum: "teal" "amber" "indigo" "red" "blue" "purple" "green" "pink" "slate" "orange" "olive" "brown"

Key of the colour palette.

contact
string or null <= 2000 characters
contact_email
string or null <email> <= 191 characters
contact_phone
string or null <= 64 characters
customer_number
string or null <= 64 characters
street
string or null <= 191 characters
address_extra
string or null <= 191 characters
postal_code
string or null <= 32 characters
city
string or null <= 191 characters
country
string or null = 2 characters
email
string or null <email> <= 191 characters
phone
string or null <= 64 characters
website
string or null <= 191 characters
vat_id
string or null <= 64 characters
tax_number
string or null <= 64 characters
notes
string or null <= 5000 characters
default_rate_cents
integer or null >= 0
rate_valid_from
string or null <date>

Day from which a new rate applies, default today.

Responses

Request samples

Content type
application/json
{
  • "name": "Müller GmbH",
  • "color": "teal",
  • "default_rate_cents": 9000
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get a client

Authorizations:
bearerAuth
path Parameters
client
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Change a client

Only the fields you send change. A new default_rate_cents applies from rate_valid_from (default today); the previous rate stays in the history.

Authorizations:
bearerAuth
path Parameters
client
required
integer >= 1
Request Body schema: application/json
required
name
string <= 191 characters
color
string (ProjectColor)
Enum: "teal" "amber" "indigo" "red" "blue" "purple" "green" "pink" "slate" "orange" "olive" "brown"

Key of the colour palette.

contact
string or null <= 2000 characters
contact_email
string or null <email> <= 191 characters
contact_phone
string or null <= 64 characters
customer_number
string or null <= 64 characters
street
string or null <= 191 characters
address_extra
string or null <= 191 characters
postal_code
string or null <= 32 characters
city
string or null <= 191 characters
country
string or null = 2 characters
email
string or null <email> <= 191 characters
phone
string or null <= 64 characters
website
string or null <= 191 characters
vat_id
string or null <= 64 characters
tax_number
string or null <= 64 characters
notes
string or null <= 5000 characters
default_rate_cents
integer or null >= 0
rate_valid_from
string or null <date>

Day from which a new rate applies, default today.

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "color": "teal",
  • "contact": "string",
  • "contact_email": "[email protected]",
  • "contact_phone": "string",
  • "customer_number": "string",
  • "street": "string",
  • "address_extra": "string",
  • "postal_code": "string",
  • "city": "string",
  • "country": "st",
  • "email": "[email protected]",
  • "phone": "string",
  • "website": "string",
  • "vat_id": "string",
  • "tax_number": "string",
  • "notes": "string",
  • "default_rate_cents": 0,
  • "rate_valid_from": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Archive a client

Clients are never deleted; this sets archived_at. Their projects stay as they are.

Authorizations:
bearerAuth
path Parameters
client
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Projects

Projects belong to exactly one client.

List projects

Team members see the projects assigned to them (or all with "all projects"), without amounts.

Authorizations:
bearerAuth
query Parameters
updated_since
string <date-time>

Only records changed at or after this time (ISO 8601 with offset).

page[size]
integer [ 1 .. 200 ]
Default: 50

Entries per page, 1 to 200.

page[cursor]
string

meta.next_cursor of the previous page.

client_id
integer >= 1
status
string (ProjectStatus)
Enum: "active" "paused" "completed" "archived"
search
string <= 100 characters

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {
    }
}

Create a project

Owners and admins. The Free plan allows three active projects (403 project_limit_reached). Fixed price and non-billable need Standard.

Authorizations:
bearerAuth
Request Body schema: application/json
required
name
string <= 191 characters
client_id
integer
ProjectColor (string) or null
status
string (ProjectStatus)
Enum: "active" "paused" "completed" "archived"
billing_mode
string (BillingMode)
Enum: "hourly" "fixed" "non_billable"
fixed_price_cents
integer or null >= 0
notes
string or null <= 5000 characters

Responses

Request samples

Content type
application/json
{
  • "name": "Website relaunch",
  • "client_id": 4
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get a project

Authorizations:
bearerAuth
path Parameters
project
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Change a project

Only the fields you send change. Setting a project back to active counts against the project limit.

Authorizations:
bearerAuth
path Parameters
project
required
integer >= 1
Request Body schema: application/json
required
name
string <= 191 characters
client_id
integer
ProjectColor (string) or null
status
string (ProjectStatus)
Enum: "active" "paused" "completed" "archived"
billing_mode
string (BillingMode)
Enum: "hourly" "fixed" "non_billable"
fixed_price_cents
integer or null >= 0
notes
string or null <= 5000 characters

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "client_id": 0,
  • "color": "teal",
  • "status": "active",
  • "billing_mode": "hourly",
  • "fixed_price_cents": 0,
  • "notes": "string"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Archive a project

Projects are never deleted; this sets the status archived.

Authorizations:
bearerAuth
path Parameters
project
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Activities

Optional activities, per project or for the whole workspace.

List activities

Authorizations:
bearerAuth
query Parameters
updated_since
string <date-time>

Only records changed at or after this time (ISO 8601 with offset).

page[size]
integer [ 1 .. 200 ]
Default: 50

Entries per page, 1 to 200.

page[cursor]
string

meta.next_cursor of the previous page.

project_id
integer >= 1

Only activities you can choose in this project (its own and the workspace-wide ones).

archived
string
Enum: "true" "false" "1" "0"

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {
    }
}

Create an activity

Owners and admins, from the Standard plan. Without project_id the activity applies to the whole workspace.

Authorizations:
bearerAuth
Request Body schema: application/json
required
name
string <= 191 characters
project_id
integer or null

Only when creating.

rate_cents
integer or null >= 0
rate_valid_from
string or null <date>

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "project_id": 0,
  • "rate_cents": 0,
  • "rate_valid_from": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get an activity

Authorizations:
bearerAuth
path Parameters
activity
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Change an activity

Authorizations:
bearerAuth
path Parameters
activity
required
integer >= 1
Request Body schema: application/json
required
name
string <= 191 characters
project_id
integer or null

Only when creating.

rate_cents
integer or null >= 0
rate_valid_from
string or null <date>

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "project_id": 0,
  • "rate_cents": 0,
  • "rate_valid_from": "2019-08-24"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Archive an activity

Authorizations:
bearerAuth
path Parameters
activity
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Tags

Free tags of the workspace, assigned to time entries by name.

List tags

Tags are assigned to time entries by name; unknown names create new tags.

Authorizations:
bearerAuth
query Parameters
updated_since
string <date-time>

Only records changed at or after this time (ISO 8601 with offset).

page[size]
integer [ 1 .. 200 ]
Default: 50

Entries per page, 1 to 200.

page[cursor]
string

meta.next_cursor of the previous page.

search
string <= 64 characters

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {
    }
}

Users

Accounts of the workspace.

List accounts

Owners and admins see the whole team, team members only themselves.

Authorizations:
bearerAuth
query Parameters
updated_since
string <date-time>

Only records changed at or after this time (ISO 8601 with offset).

page[size]
integer [ 1 .. 200 ]
Default: 50

Entries per page, 1 to 200.

page[cursor]
string

meta.next_cursor of the previous page.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {
    }
}

Get an account

Authorizations:
bearerAuth
path Parameters
user
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Time entries

Tracked time, the running timer and the delta sync.

List time entries

The same filters as the time list in the app. Team members only see their own entries and no amounts. Sorted by start, newest first (sort=-started_at) or oldest first (sort=started_at).

Authorizations:
bearerAuth
query Parameters
updated_since
string <date-time>

Only records changed at or after this time (ISO 8601 with offset).

page[size]
integer [ 1 .. 200 ]
Default: 50

Entries per page, 1 to 200.

page[cursor]
string

meta.next_cursor of the previous page.

from
string <date>

First calendar day (YYYY-MM-DD, time zone of the account).

to
string <date>

Last calendar day, inclusive.

client_id
integer >= 1
integer or string or Array of integers

One project, a comma separated list or project_id[]= several times.

activity_id
integer >= 1
user_id
integer >= 1

Owners and admins only.

status
string (EntryStatus)
Enum: "open" "billed" "locked"
billable
string
Enum: "true" "false" "1" "0"
running
string
Enum: "true" "false" "1" "0"

Only the running timer (true) or only finished entries (false).

tag_id
integer >= 1
search
string <= 100 characters

Part of the note.

sort
string
Default: "-started_at"
Enum: "started_at" "-started_at"

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {
    }
}

Create a time entry

Creates a time entry for the account of the token (source api). Without ended_at and duration_seconds it is a running timer that started at started_at, in addition to any other running timers; if the same project and activity is already running, that entry is returned (200). At most 24 hours, the break may not exceed the duration. With a client_uuid that already exists the existing entry is returned (200).

Authorizations:
bearerAuth
Request Body schema: application/json
required
project_id
integer

Required when creating.

activity_id
integer or null
started_at
string <date-time>

ISO 8601 with offset or Z. Required when creating.

ended_at
string or null <date-time>

Omit (and omit duration_seconds) for a running timer.

duration_seconds
integer or null [ 0 .. 86400 ]

Instead of ended_at.

break_seconds
integer or null >= 0
note
string or null <= 2000 characters
tags
Array of strings or null <= 20 items [ items <= 64 characters ]
billable
boolean or null

Default from the billing mode of the project.

client_uuid
string or null <uuid>

Makes creating idempotent.

rate_override_cents
integer or null >= 0

Rate for this entry only; owners and admins.

Responses

Request samples

Content type
application/json
{
  • "project_id": 12,
  • "started_at": "2026-10-14T09:00:00+02:00",
  • "ended_at": "2026-10-14T10:30:00+02:00",
  • "note": "Workshop",
  • "tags": [
    ]
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Delta sync of time entries

Every time entry changed after cursor, ordered by sync_version, with tombstones of deleted entries (deleted_at set). Premium. Team members only get their own entries.

Authorizations:
bearerAuth
query Parameters
cursor
string^\d{1,20}$

meta.next_cursor of the previous call; 0 or empty for the start.

limit
integer [ 1 .. 1000 ]
Default: 500
include_deleted
string
Default: "1"
Enum: "true" "false" "1" "0"
header Parameters
If-None-Match
string

ETag of the previous answer.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {
    }
}

Get a time entry

Authorizations:
bearerAuth
path Parameters
timeEntry
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Change a time entry

Only the fields you send change; tags replaces all tags. A finished entry stays finished, a running timer gets its end with ended_at. Billed or locked entries answer 423, entries behind the edit lock of the workspace (team members) as well.

Authorizations:
bearerAuth
path Parameters
timeEntry
required
integer >= 1
Request Body schema: application/json
required
project_id
integer

Required when creating.

activity_id
integer or null
started_at
string <date-time>

ISO 8601 with offset or Z. Required when creating.

ended_at
string or null <date-time>

Omit (and omit duration_seconds) for a running timer.

duration_seconds
integer or null [ 0 .. 86400 ]

Instead of ended_at.

break_seconds
integer or null >= 0
note
string or null <= 2000 characters
tags
Array of strings or null <= 20 items [ items <= 64 characters ]
billable
boolean or null

Default from the billing mode of the project.

client_uuid
string or null <uuid>

Makes creating idempotent.

rate_override_cents
integer or null >= 0

Rate for this entry only; owners and admins.

Responses

Request samples

Content type
application/json
{
  • "project_id": 0,
  • "activity_id": 0,
  • "started_at": "2019-08-24T14:15:22Z",
  • "ended_at": "2019-08-24T14:15:22Z",
  • "duration_seconds": 86400,
  • "break_seconds": 0,
  • "note": "string",
  • "tags": [
    ],
  • "billable": true,
  • "client_uuid": "c484e8ef-f25a-4dd9-ab89-f54a430ba3fc",
  • "rate_override_cents": 0
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Delete a time entry

The entry disappears from lists and appears as a tombstone in the delta sync.

Authorizations:
bearerAuth
path Parameters
timeEntry
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

Reports

Totals with the same numbers as the reports in the app.

Totals for a period

Totals of finished time entries, grouped by client, project, activity, account (owners and admins) or day, with the same numbers as the reports, the dashboard and the exports. Without from and to the current month in the time zone of the account.

Authorizations:
bearerAuth
query Parameters
from
string <date>
to
string <date>
group_by
string
Default: "project"
Enum: "client" "project" "activity" "user" "day"
client_id
integer >= 1
integer or string or Array of integers
activity_id
integer >= 1
user_id
integer >= 1
status
string (EntryStatus)
Enum: "open" "billed" "locked"
billable
string
Enum: "true" "false" "1" "0"
tag_id
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    },
  • "meta": {
    }
}

Billing runs

Mark time entries as billed with an invoice reference and lock them.

List billing runs

Newest first. Owners and admins, ability billing.

Authorizations:
bearerAuth
query Parameters
updated_since
string <date-time>

Only records changed at or after this time (ISO 8601 with offset).

page[size]
integer [ 1 .. 200 ]
Default: 50

Entries per page, 1 to 200.

page[cursor]
string

meta.next_cursor of the previous page.

client_id
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {
    }
}

Create a billing run

Marks time entries as billed with the invoice reference of your ERP and locks them. Either entry_ids (exactly these entries of one client; already billed or locked ones answer 409 with their list, running or non-billable ones 422) or filter (all open, finished, billable entries of a client up to a day, optionally of one project; others are skipped and counted). With dry_run: true nothing is written and the answer shows what would happen. Mixed currencies in one run answer 422.

Authorizations:
bearerAuth
Request Body schema: application/json
required
entry_ids
Array of integers [ 1 .. 1000 ] items
object
invoice_ref
required
string <= 191 characters
invoiced_at
string or null <date>

Invoice date, default today.

dry_run
boolean
Default: false

Responses

Request samples

Content type
application/json
Example
{
  • "entry_ids": [
    ],
  • "invoice_ref": "RE-2026-0042",
  • "invoiced_at": "2026-10-31"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get a billing run

Authorizations:
bearerAuth
path Parameters
billingRun
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    },
  • "meta": {
    }
}

Cancel a billing run

Its time entries are open again and recalculated with today's rules; the run stays with cancelled_at as a record.

Authorizations:
bearerAuth
path Parameters
billingRun
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    },
  • "meta": {
    }
}

Trigger

Start, stop, toggle and switch the timer from buttons, shortcuts and automations.

Current timer

The running timers of the account of the token (several may run in parallel). Ability read or track (device tokens).

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Status to read aloud

Tracked time today (rounded, own entries, the same numbers as the reports) plus the running timer, and a short sentence in the language of the account (not of the request), for iOS Shortcuts ("Speak Text"), Siri and voice assistants. Ability read or track (device tokens). With format=text the answer is only the sentence as text/plain.

Authorizations:
bearerAuth
query Parameters
format
string
Default: "json"
Enum: "json" "text"

Responses

Response samples

Content type
{
  • "data": {
    }
}

Bookable projects for a tracker

Everything a tracker such as Horaka Desktop needs to offer a choice: bookable projects (active and paused, visible to the account, not of archived clients, for device tokens only the allowed ones) with client and selectable activities, sorted by client and project, plus the favourites and recently used combinations of the account and whether parallel timers are on. Never amounts. Ability read or track (device tokens).

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Start the timer

Starts the project in addition to any running timers. If it is already running (with the same activity, if given), nothing changes.

Authorizations:
bearerAuth
header Parameters
Idempotency-Key
string <= 191 characters

Unique text per press (1 to 191 visible ASCII characters), valid for 24 hours per token.

Request Body schema: application/json
required
integer or string

Project ID or alias. Required for start and switch.

integer or string or null

Activity ID or name; an alias can bring its activity.

note
string or null <= 2000 characters

Responses

Request samples

Content type
application/json
{
  • "project": "MUE-WEB"
}

Response samples

Content type
application/json
{
  • "data": {
    },
  • "meta": {
    }
}

Change the note of an own entry

For trackers such as Horaka Desktop: sets the note of an own time entry that is running or ended at most 24 hours ago, for example a summary right after stopping. An empty note removes it. Ability track; device tokens only on their projects. Locked entries and older ones answer 423, entries of others 404.

Authorizations:
bearerAuth
path Parameters
timeEntry
required
integer >= 1
Request Body schema: application/json
required
note
required
string or null <= 2000 characters

Responses

Request samples

Content type
application/json
{
  • "note": "Workshop prepared, slides sent"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Stop the timer

Stops all running timers, or with project only the running timers of that project; without a matching timer nothing happens (meta.action is none). Device tokens only stop timers on their projects.

Authorizations:
bearerAuth
header Parameters
Idempotency-Key
string <= 191 characters

Unique text per press (1 to 191 visible ASCII characters), valid for 24 hours per token.

Request Body schema: application/json
optional
integer or string

Project ID or alias. Required for start and switch.

integer or string or null

Activity ID or name; an alias can bring its activity.

note
string or null <= 2000 characters

Responses

Request samples

Content type
application/json
{
  • "project": 0,
  • "activity": 0,
  • "note": "string"
}

Response samples

Content type
application/json
{
  • "data": {
    },
  • "meta": {
    }
}

Toggle the timer

If the given project is running, its timers stop; otherwise the given project starts in addition to running timers. Without a project, all running timers stop. Ideal for one button per project.

Authorizations:
bearerAuth
header Parameters
Idempotency-Key
string <= 191 characters

Unique text per press (1 to 191 visible ASCII characters), valid for 24 hours per token.

Request Body schema: application/json
optional
integer or string

Project ID or alias. Required for start and switch.

integer or string or null

Activity ID or name; an alias can bring its activity.

note
string or null <= 2000 characters

Responses

Request samples

Content type
application/json
{
  • "project": 0,
  • "activity": 0,
  • "note": "string"
}

Response samples

Content type
application/json
{
  • "data": {
    },
  • "meta": {
    }
}

Switch the timer

Ends all other running timers now and starts the given project at the same moment (if it is already running, it keeps running). Device tokens only stop timers on their projects.

Authorizations:
bearerAuth
header Parameters
Idempotency-Key
string <= 191 characters

Unique text per press (1 to 191 visible ASCII characters), valid for 24 hours per token.

Request Body schema: application/json
required
integer or string

Project ID or alias. Required for start and switch.

integer or string or null

Activity ID or name; an alias can bring its activity.

note
string or null <= 2000 characters

Responses

Request samples

Content type
application/json
{
  • "project": 0,
  • "activity": 0,
  • "note": "string"
}

Response samples

Content type
application/json
{
  • "data": {
    },
  • "meta": {
    }
}

Webhooks

Outgoing webhook endpoints.

List webhook endpoints

Owners and admins, ability webhooks.

Authorizations:
bearerAuth
query Parameters
updated_since
string <date-time>

Only records changed at or after this time (ISO 8601 with offset).

page[size]
integer [ 1 .. 200 ]
Default: 50

Entries per page, 1 to 200.

page[cursor]
string

meta.next_cursor of the previous page.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    },
  • "links": {
    }
}

Register a webhook endpoint

The URL must use https and may not point into a private network. The secret (generated unless you send one) is only part of this answer.

Authorizations:
bearerAuth
Request Body schema: application/json
required
url
required
string <uri> <= 2000 characters
events
required
Array of strings (WebhookEventName) non-empty
Items Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
secret
string [ 16 .. 191 ] characters

Own secret; generated when missing.

Responses

Request samples

Content type
application/json
{}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get a webhook endpoint

Authorizations:
bearerAuth
path Parameters
webhook
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Delete a webhook endpoint

Authorizations:
bearerAuth
path Parameters
webhook
required
integer >= 1

Responses

Response samples

Content type
application/json
{
  • "errors": [
    ]
}

A time entry was created Webhook

Also for a timer that starts (then timer.started follows).

Authorizations:
bearerAuth
header Parameters
X-Horaka-Signature
required
string

t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">

X-Horaka-Event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
X-Horaka-Delivery-Id
required
string <uuid>

Unique per delivery attempt chain; a replay gets a new one.

Request Body schema: application/json
id
required
string

ID of the event; stays the same on a replay.

event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
created_at
required
string <date-time>
api_version
required
string
Value: "v1"
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "event": "time_entry.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "api_version": "v1",
  • "data": {
    }
}

A time entry was changed Webhook

Also when the server recalculates rounding or amounts.

Authorizations:
bearerAuth
header Parameters
X-Horaka-Signature
required
string

t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">

X-Horaka-Event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
X-Horaka-Delivery-Id
required
string <uuid>

Unique per delivery attempt chain; a replay gets a new one.

Request Body schema: application/json
id
required
string

ID of the event; stays the same on a replay.

event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
created_at
required
string <date-time>
api_version
required
string
Value: "v1"
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "event": "time_entry.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "api_version": "v1",
  • "data": {
    }
}

A time entry was deleted Webhook

Authorizations:
bearerAuth
header Parameters
X-Horaka-Signature
required
string

t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">

X-Horaka-Event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
X-Horaka-Delivery-Id
required
string <uuid>

Unique per delivery attempt chain; a replay gets a new one.

Request Body schema: application/json
id
required
string

ID of the event; stays the same on a replay.

event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
created_at
required
string <date-time>
api_version
required
string
Value: "v1"
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "event": "time_entry.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "api_version": "v1",
  • "data": {
    }
}

A time entry was billed or locked Webhook

Authorizations:
bearerAuth
header Parameters
X-Horaka-Signature
required
string

t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">

X-Horaka-Event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
X-Horaka-Delivery-Id
required
string <uuid>

Unique per delivery attempt chain; a replay gets a new one.

Request Body schema: application/json
id
required
string

ID of the event; stays the same on a replay.

event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
created_at
required
string <date-time>
api_version
required
string
Value: "v1"
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "event": "time_entry.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "api_version": "v1",
  • "data": {
    }
}

A timer started Webhook

Authorizations:
bearerAuth
header Parameters
X-Horaka-Signature
required
string

t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">

X-Horaka-Event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
X-Horaka-Delivery-Id
required
string <uuid>

Unique per delivery attempt chain; a replay gets a new one.

Request Body schema: application/json
id
required
string

ID of the event; stays the same on a replay.

event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
created_at
required
string <date-time>
api_version
required
string
Value: "v1"
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "event": "time_entry.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "api_version": "v1",
  • "data": {
    }
}

A timer stopped Webhook

Authorizations:
bearerAuth
header Parameters
X-Horaka-Signature
required
string

t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">

X-Horaka-Event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
X-Horaka-Delivery-Id
required
string <uuid>

Unique per delivery attempt chain; a replay gets a new one.

Request Body schema: application/json
id
required
string

ID of the event; stays the same on a replay.

event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
created_at
required
string <date-time>
api_version
required
string
Value: "v1"
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "event": "time_entry.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "api_version": "v1",
  • "data": {
    }
}

A billing run was created Webhook

Authorizations:
bearerAuth
header Parameters
X-Horaka-Signature
required
string

t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">

X-Horaka-Event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
X-Horaka-Delivery-Id
required
string <uuid>

Unique per delivery attempt chain; a replay gets a new one.

Request Body schema: application/json
id
required
string

ID of the event; stays the same on a replay.

event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
created_at
required
string <date-time>
api_version
required
string
Value: "v1"
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "event": "time_entry.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "api_version": "v1",
  • "data": {
    }
}

A billing run was cancelled Webhook

Authorizations:
bearerAuth
header Parameters
X-Horaka-Signature
required
string

t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">

X-Horaka-Event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
X-Horaka-Delivery-Id
required
string <uuid>

Unique per delivery attempt chain; a replay gets a new one.

Request Body schema: application/json
id
required
string

ID of the event; stays the same on a replay.

event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
created_at
required
string <date-time>
api_version
required
string
Value: "v1"
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "event": "time_entry.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "api_version": "v1",
  • "data": {
    }
}

A project budget reached a threshold Webhook

Once per project, kind (time or money) and threshold (the warning threshold of the project and 100 percent).

Authorizations:
bearerAuth
header Parameters
X-Horaka-Signature
required
string

t=<unix time>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">

X-Horaka-Event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
X-Horaka-Delivery-Id
required
string <uuid>

Unique per delivery attempt chain; a replay gets a new one.

Request Body schema: application/json
id
required
string

ID of the event; stays the same on a replay.

event
required
string (WebhookEventName)
Enum: "time_entry.created" "time_entry.updated" "time_entry.deleted" "time_entry.locked" "timer.started" "timer.stopped" "billing_run.created" "billing_run.cancelled" "project.budget_threshold"
created_at
required
string <date-time>
api_version
required
string
Value: "v1"
required
object

Responses

Request samples

Content type
application/json
{
  • "id": "string",
  • "event": "time_entry.created",
  • "created_at": "2019-08-24T14:15:22Z",
  • "api_version": "v1",
  • "data": {
    }
}