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
Public API access is a Pro feature. You issue and revoke API keys from your dashboard under Settings → API Keys.

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 header
Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2

Getting a key

  1. Open your dashboard and go to Settings → API Keys.
  2. Create a key, give it a name (e.g. “Zapier”), and choose a scope.
  3. 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.

Treat keys like passwords. Store them in a secrets manager, never commit them, and revoke any key you suspect is exposed. A revoked key (and all of its webhook subscriptions) stops working immediately.

Quickstart

From zero to a tracked session in four calls.

Test your key
# 1. Verify your key
curl https://temporal.ist/api/v1/me/ \
  -H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2"
Create a project and track time
# 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:

ScopeAllows
readSafe methods only — GET, HEAD, OPTIONS.
writeEverything 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.

StatusMeaning
200 OKRequest succeeded.
201 CreatedResource created.
204 No ContentResource deleted; no body returned.
400 Bad RequestValidation failed. Body is keyed by field name.
401 UnauthorizedMissing, malformed, invalid, or revoked API key.
403 ForbiddenKey scope is insufficient, or the action needs an organisation / Pro plan.
404 Not FoundNo such resource, or it is not visible to your key.
Validation error (400)
{
  "period_start": ["This field is required."],
  "client_id": ["This field is required."]
}

Identity

GET /me/ read scope

Return 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.

Example request
curl https://temporal.ist/api/v1/me/ \
  -H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2"
Example response
{
  "user_id": 42,
  "email": "you@example.com",
  "organisation_id": 7,
  "organisation_name": "Dunder Mifflin",
  "api_key_name": "Zapier",
  "api_key_scope": "write"
}

Projects

GET /projects/ read scope

List the projects you own or that belong to one of your teams, newest first.

Example response
[
  {
    "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"
  }
]
POST /projects/ write scope

Create a project. It is assigned to the calling user and their organisation.

Body parameters

FieldTypeRequiredDescription
namestringYesDisplay name of the project.
is_activebooleanWhether the project tracks new time. Defaults to false.
currencystringISO currency code, e.g. "USD".
hourly_ratedecimal stringBilling rate. May be null.
client_iduuidLink the project to a client (by the client’s UUID) for invoicing. May be null.
Example request
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"}'
Example response
{
  "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "name": "Website redesign",
  "is_active": false,
  "currency": "USD",
  "hourly_rate": "120.00",
  "client_id": null
}
GET /projects/{id}/ read scope

Retrieve a single project by its UUID.

PATCH /projects/{id}/ write scope

Update one or more mutable fields. Send only the fields you want to change.

Body parameters

FieldTypeRequiredDescription
namestringNew display name.
is_activebooleanActivate or deactivate the project.
currencystringISO currency code.
hourly_ratedecimal stringBilling rate, or null.
client_iduuidLinked client UUID, or null.
Example request
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

GET /sessions/ read scope

List your tracked sessions, newest first.

Query parameters

FieldTypeRequiredDescription
active"true" | "false"Filter to running (true) or finished (false) sessions.
project_iduuidOnly sessions for the given project.
Example request
curl "https://temporal.ist/api/v1/sessions/?active=true" \
  -H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2"
Example response
[
  {
    "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
  }
]
POST /sessions/ write scope

Create 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

FieldTypeRequiredDescription
project_iduuidYesThe project this session belongs to.
start_timeISO 8601When the session began. Defaults to the current time.
end_timeISO 8601When the session ended. Omit to leave it running.
descriptionstringFree-text note for the session.
started_viaenumHow 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.
Example request
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"}'
Example response
{
  "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
}
GET /sessions/{id}/ read scope

Retrieve a single session by its UUID.

PATCH /sessions/{id}/ write scope

Update a session — for example set end_time or amend the description.

Body parameters

FieldTypeRequiredDescription
end_timeISO 8601Set or change when the session ended.
descriptionstringUpdate the note.
project_iduuidReassign the session to another project you can access.
DELETE /sessions/{id}/ write scope

Permanently delete a session. Returns 204 No Content.

POST /sessions/{id}/close/ write scope

End a running session now (sets end_time to the current time). Idempotent: closing an already-closed session returns 200 rather than an error.

Example request
curl -X POST https://temporal.ist/api/v1/sessions/9f8e7d6c-5b4a-4039-8271-6a5b4c3d2e1f/close/ \
  -H "Authorization: Bearer tk_live_7g3k9m2p_q8x4w1z6r5t0y9u3i7o2a6s1d4f8g0h2"
Example response
{
  "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
}
On the idempotent path the body has a different shape: when the session was already closed, the response is wrapped as { "detail": "Session is already closed.", "session": { …the session object… } } instead of a bare session object. Read the session from the "session" key when retrying close().

Clients

GET /clients/ read scope

List the clients in your organisation, ordered by name. Returns an empty list if your account has no organisation.

Example response
[
  {
    "id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
    "name": "Globex Corp",
    "company": "Globex",
    "email": "billing@globex.example",
    "hourly_rate": "150.00",
    "currency": "USD"
  }
]
POST /clients/ write scope

Create a client in your organisation. Requires the calling account to belong to an organisation, otherwise returns 403.

Body parameters

FieldTypeRequiredDescription
namestringYesClient name.
companystringCompany name.
emailstringBilling email.
hourly_ratedecimal stringDefault billing rate. May be null.
currencystringISO currency code.
Example request
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"}'
GET /clients/{id}/ read scope

Retrieve a single client by its UUID.

PATCH /clients/{id}/ write scope

Update one or more client fields.

Body parameters

FieldTypeRequiredDescription
namestringClient name.
companystringCompany name.
emailstringBilling email.
hourly_ratedecimal stringDefault billing rate, or null.
currencystringISO currency code.

Invoices

GET /invoices/ read scope

List invoices in your organisation, newest first.

Query parameters

FieldTypeRequiredDescription
statusstringFilter by status, e.g. draft, sent, paid.
client_iduuidOnly invoices for the given client UUID.
Example response
[
  {
    "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"
  }
]
POST /invoices/ write scope

Create 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

FieldTypeRequiredDescription
client_iduuidYesUUID of the client to bill.
period_startdate (YYYY-MM-DD)YesStart of the billed period.
period_enddate (YYYY-MM-DD)YesEnd of the billed period.
project_idsuuid[]Restrict to these projects. Defaults to all the client’s projects.
tax_ratedecimalTax percentage. Defaults to 0.
notesstringFree-text notes on the invoice.
overlap_strategy"add" | "merge"How to handle overlapping sessions. Defaults to "add".
Example request
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"
  }'
GET /invoices/{id}/ read scope

Retrieve a single invoice by its UUID.

POST /invoices/{id}/send/ write scope

Mark an invoice as sent. Only valid from the draft or sent state; email delivery is handled separately by the app.

POST /invoices/{id}/mark_paid/ write scope

Mark an invoice as paid and stamp today as the paid date. No-op if it is already paid.

Webhooks

GET /webhooks/ read scope

List the webhook subscriptions created with the calling API key.

Example response
[
  {
    "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
  }
]
POST /webhooks/ write scope

Subscribe 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

FieldTypeRequiredDescription
eventstringYesOne of project.created, session.created, session.closed, invoice.created, invoice.paid.
target_urlurlYesHTTPS URL that will receive the POST.
Example request
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"}'
Example response
{
  "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
}
GET /webhooks/{id}/ read scope

Retrieve a single subscription by its UUID.

DELETE /webhooks/{id}/ write scope

Delete 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

EventFires when
project.createdA new project was created.
session.createdA session was started.
session.closedA running session was ended.
invoice.createdA new invoice was created.
invoice.paidAn invoice transitioned to paid.

Delivery payload

Each delivery is a JSON envelope. The data object’s shape depends on the event:

POST body
{
  "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

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.

Verify a signature (Python)
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)
Deliveries are best-effort and fire-and-forget, with an 8-second timeout and no automatic retries. Respond 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=json or send Accept: application/json to get JSON.
  • Interactive explorer — /api/v1/docs/