API Reference
The stable v1 REST API for integrating Temporal.ist with Zapier, Make, n8n or your own code.
Introduction
The Temporal.ist public API is a JSON-over-HTTPS REST API for automating your time tracking and invoicing. It is versioned and stable: field names and shapes are part of the contract and won’t change within v1.
What you can do
- Read and manage projects, sessions, and clients
- Create and progress invoices from tracked time
- Subscribe to webhooks for real-time events
Base URL
All endpoints live under the /api/v1/ prefix:
- Production —
https://temporal.ist/api/v1 - Local development —
http://localhost:8000/api/v1
Authentication
Every request authenticates with a bearer token in the Authorization header.
There is no OAuth dance for the public API — a long-lived key is all you need.
Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2Getting a key
- Open your dashboard and go to Settings → API Keys.
- Create a key, give it a name (e.g. “Zapier”), and choose a scope.
- Copy the token immediately — it is shown once and only a hash is stored. We can’t recover it for you.
Tokens look like tk_live_<id>_<secret> — an 8-character public id and a
32-character secret. To rotate a key, create a new one and revoke the old one.
Quickstart
From zero to a tracked session in four calls.
# 1. Verify your key
curl https://temporal.ist/api/v1/me/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2"# 2. Create a project
curl -X POST https://temporal.ist/api/v1/projects/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2" \
-H "Content-Type: application/json" \
-d '{"name": "Website redesign"}'
# 3. Start tracking time against it
curl -X POST https://temporal.ist/api/v1/sessions/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2" \
-H "Content-Type: application/json" \
-d '{"project_id": "<project id from step 2>"}'
# 4. Stop it later
curl -X POST https://temporal.ist/api/v1/sessions/<session id>/close/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2"Requests & scopes
Send and receive JSON. Include Content-Type: application/json on requests
with a body. All responses are JSON.
Scopes
Each key carries one scope, checked on every request:
| Scope | Allows |
|---|---|
| read | Safe methods only — GET, HEAD, OPTIONS. |
| write | Everything a read key can do, plus POST, PATCH and DELETE. |
Using a write-only method with a read-scoped key returns 403 Forbidden.
Resource identifiers
Every resource — projects, sessions, clients, invoices and webhook subscriptions — is
addressed by a UUID. Wherever a field references a client (for example client_id on a project or invoice) it carries that client’s UUID, not an
integer.
Rate limits
There are currently no hard rate limits on the public API. Please be a good
citizen — batch where you can and avoid tight polling; prefer webhooks for
real-time updates. Limits may be introduced later, so handle 429 responses defensively.
Errors
The API uses standard HTTP status codes. Auth, permission and not-found errors return a { "detail": "…" } body; validation errors return an object keyed by field name.
| Status | Meaning |
|---|---|
200 OK | Request succeeded. |
201 Created | Resource created. |
204 No Content | Resource deleted; no body returned. |
400 Bad Request | Validation failed. Body is keyed by field name. |
401 Unauthorized | Missing, malformed, invalid, or revoked API key. |
403 Forbidden | Key scope is insufficient, or the action needs an organisation / Pro plan. |
404 Not Found | No such resource, or it is not visible to your key. |
{
"period_start": ["This field is required."],
"client_id": ["This field is required."]
}Identity
/me/ read scopeReturn the account and organisation your key is attached to, plus the key’s name and scope. Use it as a connection / auth test before issuing other calls.
curl https://temporal.ist/api/v1/me/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2"{
"user_id": 42,
"email": "you@example.com",
"organisation_id": 7,
"organisation_name": "Dunder Mifflin",
"api_key_name": "Zapier",
"api_key_scope": "write"
}Projects
/projects/ read scopeList the projects you own or that belong to one of your teams, newest first.
[
{
"id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Website redesign",
"is_active": true,
"currency": "USD",
"hourly_rate": "120.00",
"client_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
}
]/projects/ write scopeCreate a project. It is assigned to the calling user and their organisation.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name of the project. |
is_active | boolean | — | Whether the project tracks new time. Defaults to false. |
currency | string | — | ISO currency code, e.g. "USD". |
hourly_rate | decimal string | — | Billing rate. May be null. |
client_id | uuid | — | Link the project to a client (by the client’s UUID) for invoicing. May be null. |
curl -X POST https://temporal.ist/api/v1/projects/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2" \
-H "Content-Type: application/json" \
-d '{"name": "Website redesign", "currency": "USD", "hourly_rate": "120.00"}'{
"id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Website redesign",
"is_active": false,
"currency": "USD",
"hourly_rate": "120.00",
"client_id": null
}/projects/{id}/ read scopeRetrieve a single project by its UUID.
/projects/{id}/ write scopeUpdate one or more mutable fields. Send only the fields you want to change.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
name | string | — | New display name. |
is_active | boolean | — | Activate or deactivate the project. |
currency | string | — | ISO currency code. |
hourly_rate | decimal string | — | Billing rate, or null. |
client_id | uuid | — | Linked client UUID, or null. |
curl -X PATCH https://temporal.ist/api/v1/projects/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2" \
-H "Content-Type: application/json" \
-d '{"is_active": false}'Sessions
/sessions/ read scopeList your tracked sessions, newest first.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
active | "true" | "false" | — | Filter to running (true) or finished (false) sessions. |
project_id | uuid | — | Only sessions for the given project. |
curl "https://temporal.ist/api/v1/sessions/?active=true" \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2"[
{
"id": "9f8e7d6c-5b4a-4039-8271-6a5b4c3d2e1f",
"project_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"project_name": "Website redesign",
"start_time": "2026-06-26T09:00:00Z",
"end_time": null,
"description": "",
"started_via": "api",
"is_active": true,
"total_seconds": 0
}
]/sessions/ write scopeCreate a session. Omit end_time to start a session that is running now; include both start_time and end_time to log a completed session. started_via defaults to "api".
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
project_id | uuid | Yes | The project this session belongs to. |
start_time | ISO 8601 | — | When the session began. Defaults to the current time. |
end_time | ISO 8601 | — | When the session ended. Omit to leave it running. |
description | string | — | Free-text note for the session. |
started_via | enum | — | How the session was started. Must be one of manual, client-file, client-manual, browser-url, browser-manual, cli-tui, cli-watch, api; any other value returns 400. Defaults to "api" — integrations can leave it unset. |
curl -X POST https://temporal.ist/api/v1/sessions/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2" \
-H "Content-Type: application/json" \
-d '{"project_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"}'{
"id": "9f8e7d6c-5b4a-4039-8271-6a5b4c3d2e1f",
"project_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"project_name": "Website redesign",
"start_time": "2026-06-26T09:00:00Z",
"end_time": null,
"description": "",
"started_via": "api",
"is_active": true,
"total_seconds": 0
}/sessions/{id}/ read scopeRetrieve a single session by its UUID.
/sessions/{id}/ write scopeUpdate a session — for example set end_time or amend the description.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
end_time | ISO 8601 | — | Set or change when the session ended. |
description | string | — | Update the note. |
project_id | uuid | — | Reassign the session to another project you can access. |
/sessions/{id}/ write scopePermanently delete a session. Returns 204 No Content.
/sessions/{id}/close/ write scopeEnd a running session now (sets end_time to the current time). Idempotent: closing an already-closed session returns 200 rather than an error.
curl -X POST https://temporal.ist/api/v1/sessions/9f8e7d6c-5b4a-4039-8271-6a5b4c3d2e1f/close/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2"{
"id": "9f8e7d6c-5b4a-4039-8271-6a5b4c3d2e1f",
"project_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"project_name": "Website redesign",
"start_time": "2026-06-26T09:00:00Z",
"end_time": "2026-06-26T11:30:00Z",
"description": "",
"started_via": "api",
"is_active": false,
"total_seconds": 9000
}Clients
/clients/ read scopeList the clients in your organisation, ordered by name. Returns an empty list if your account has no organisation.
[
{
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"name": "Globex Corp",
"company": "Globex",
"email": "billing@globex.example",
"hourly_rate": "150.00",
"currency": "USD"
}
]/clients/ write scopeCreate a client in your organisation. Requires the calling account to belong to an organisation, otherwise returns 403.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Client name. |
company | string | — | Company name. |
email | string | — | Billing email. |
hourly_rate | decimal string | — | Default billing rate. May be null. |
currency | string | — | ISO currency code. |
curl -X POST https://temporal.ist/api/v1/clients/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2" \
-H "Content-Type: application/json" \
-d '{"name": "Globex Corp", "email": "billing@globex.example"}'/clients/{id}/ read scopeRetrieve a single client by its UUID.
/clients/{id}/ write scopeUpdate one or more client fields.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
name | string | — | Client name. |
company | string | — | Company name. |
email | string | — | Billing email. |
hourly_rate | decimal string | — | Default billing rate, or null. |
currency | string | — | ISO currency code. |
Invoices
/invoices/ read scopeList invoices in your organisation, newest first.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
status | string | — | Filter by status, e.g. draft, sent, paid. |
client_id | uuid | — | Only invoices for the given client UUID. |
[
{
"id": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"invoice_number": "DMI-202606-0001",
"client_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"status": "draft",
"subtotal": "1800.00",
"tax_rate": "21.00",
"tax_amount": "378.00",
"total": "2178.00",
"currency": "USD",
"period_start": "2026-06-01",
"period_end": "2026-06-30",
"due_date": "2026-07-15",
"paid_date": null,
"created": "2026-06-26T12:00:00Z"
}
]/invoices/ write scopeCreate an invoice for a client over a date range. Line items are generated from the time tracked in the period; pass project_ids to limit it to specific projects.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
client_id | uuid | Yes | UUID of the client to bill. |
period_start | date (YYYY-MM-DD) | Yes | Start of the billed period. |
period_end | date (YYYY-MM-DD) | Yes | End of the billed period. |
project_ids | uuid[] | — | Restrict to these projects. Defaults to all the client’s projects. |
tax_rate | decimal | — | Tax percentage. Defaults to 0. |
notes | string | — | Free-text notes on the invoice. |
overlap_strategy | "add" | "merge" | — | How to handle overlapping sessions. Defaults to "add". |
curl -X POST https://temporal.ist/api/v1/invoices/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2" \
-H "Content-Type: application/json" \
-d '{
"client_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"period_start": "2026-06-01",
"period_end": "2026-06-30",
"tax_rate": "21.00"
}'/invoices/{id}/ read scopeRetrieve a single invoice by its UUID.
/invoices/{id}/send/ write scopeMark an invoice as sent. Only valid from the draft or sent state; email delivery is handled separately by the app.
/invoices/{id}/mark_paid/ write scopeMark an invoice as paid and stamp today as the paid date. No-op if it is already paid.
Webhooks
/webhooks/ read scopeList the webhook subscriptions created with the calling API key.
[
{
"id": "b2c3d4e5-6f70-4819-a2b3-c4d5e6f70819",
"event": "session.closed",
"target_url": "https://hooks.example.com/temporalist",
"secret": "9b1c…(64 hex chars)",
"created_at": "2026-06-26T12:00:00Z",
"last_delivery_at": null,
"last_status_code": null,
"failure_count": 0
}
]/webhooks/ write scopeSubscribe a URL to an event. The response includes the per-subscription signing secret — store it to verify delivery signatures. The subscription is tied to the API key that created it.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
event | string | Yes | One of project.created, session.created, session.closed, invoice.created, invoice.paid. |
target_url | url | Yes | HTTPS URL that will receive the POST. |
curl -X POST https://temporal.ist/api/v1/webhooks/ \
-H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2" \
-H "Content-Type: application/json" \
-d '{"event": "session.closed", "target_url": "https://hooks.example.com/temporalist"}'{
"id": "b2c3d4e5-6f70-4819-a2b3-c4d5e6f70819",
"event": "session.closed",
"target_url": "https://hooks.example.com/temporalist",
"secret": "9b1c2d3e…(64 hex chars — shown so you can verify signatures)",
"created_at": "2026-06-26T12:00:00Z",
"last_delivery_at": null,
"last_status_code": null,
"failure_count": 0
}/webhooks/{id}/ read scopeRetrieve a single subscription by its UUID.
/webhooks/{id}/ write scopeDelete a subscription. Returns 204 No Content. Revoking the API key removes its subscriptions automatically.
Working with webhooks
Subscribe a URL to an event and Temporal.ist will POST a JSON payload to it
whenever that event happens. Subscriptions belong to the API key that created them.
Events
| Event | Fires when |
|---|---|
project.created | A new project was created. |
session.created | A session was started. |
session.closed | A running session was ended. |
invoice.created | A new invoice was created. |
invoice.paid | An invoice transitioned to paid. |
Delivery payload
Each delivery is a JSON envelope. The data object’s shape depends on the event:
{
"event": "session.closed",
"delivery_id": "f0e1d2c3-b4a5-4697-8889-0a1b2c3d4e5f",
"occurred_at": 1781434200,
"data": {
"uuid": "9f8e7d6c-5b4a-4039-8271-6a5b4c3d2e1f",
"project_uuid": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"project_name": "Website redesign",
"start_time": "2026-06-26T09:00:00+00:00",
"end_time": "2026-06-26T11:30:00+00:00",
"description": "",
"started_via": "api"
}
}Delivery headers
Content-Type: application/json
User-Agent: Temporalist-Webhook/1.0
X-Temporalist-Event: session.closed
X-Temporalist-Delivery: f0e1d2c3-b4a5-4697-8889-0a1b2c3d4e5f
X-Temporalist-Signature: t=1781434200,v1=5257a869e6…X-Temporalist-Delivery is a UUID you can use as an idempotency key —
the same delivery may, in rare cases, arrive more than once.
Verifying signatures
Each delivery is signed with the subscription’s secret using the
Stripe-compatible scheme: compute HMAC-SHA256 over "<timestamp>.<raw_body>" and compare it, in constant time, to the v1 value in X-Temporalist-Signature.
import hmac, hashlib
def verify(secret: str, signature_header: str, raw_body: bytes) -> bool:
# signature_header looks like: "t=1781434200,v1=5257a869e6…"
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp, given = parts["t"], parts["v1"]
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, given)2xx quickly; do any heavy work asynchronously.
Repeated failures increase the subscription’s failure_count.OpenAPI schema
A machine-readable description of every endpoint is available for generating typed clients and importing into tools like Postman or Insomnia.
- Schema —
/api/v1/openapi.json. Served as OpenAPI 3 YAML by default (content-negotiated); append?format=jsonor sendAccept: application/jsonto get JSON. - Interactive explorer —
/api/v1/docs/
