# REST API

Base URL `https://app.sumhour.com/api/v1`. JSON in, JSON out, camelCase names. The full machine-readable description is [openapi.json](https://app.sumhour.com/api/v1/openapi.json).

## Authentication

Send a personal API token from **API tokens** in the app:

```
Authorization: Bearer sh_pat_...
```

A token acts as its owner in one workspace. It reaches what the owner's role allows, narrowed by the token's permissions. A missing permission is `403 insufficient_scope`; a role that is too low is `403 forbidden`.

## Responses and errors

- One result: `{ "data": { ... } }`. A list with pages: `{ "data": [ ... ], "next_cursor": "…" | null }`; pass `cursor` back to get the next page.
- Errors: `{ "error": { "code": "...", "message": "...", "details": { "field": "why" } } }` with status 400, 401, 403, 404, 405, 409, 422 or 429. The message is written for people; show it as is.
- Every response has a `Sumhour-Request-Id` header. Quote it when asking for help.

## Retries and limits

- Send `Idempotency-Key: <any unique string>` on writes. Repeating the same request with the same key returns the first response (header `Idempotent-Replayed: true`) instead of doing it twice; the same key with a different body is `422`. Keys are kept 24 hours.
- 120 requests a minute per token. Over that you get `429` with `Retry-After`, plus `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` on every response.
- Durations accept `1:30`, `1.5h`, `90m` or `seconds`. Dates are `YYYY-MM-DD` in the person's timezone.

## Account

### Who am I

`GET /api/v1/me`

The signed-in person, the workspace, their role, timezone, week start and currency, and the scopes this caller has.

No parameters.

```bash
curl "https://app.sumhour.com/api/v1/me" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

## Projects and clients

### List projects

`GET /api/v1/projects` · permission `projects:read`

Active projects the caller can log time to (everything for admins), each with its client, code, tasks and whether notes are required. Use the ids when logging time.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `q` | query | string |  | Filter by project, client or code |

```bash
curl "https://app.sumhour.com/api/v1/projects" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### A project's tasks

`GET /api/v1/projects/{projectId}/tasks` · permission `projects:read`

The tasks of one project the caller can log time to, for picking a task when logging time. Make and other tools fill their task lists from it.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `projectId` | path | id | yes |  |

```bash
curl "https://app.sumhour.com/api/v1/projects/<projectId>/tasks" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### List clients

`GET /api/v1/clients` · permission `projects:read`

Active clients with their billing currency.

No parameters.

```bash
curl "https://app.sumhour.com/api/v1/clients" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### List people

`GET /api/v1/people` · permission `team:read`

Everyone in the workspace with their role.

No parameters.

```bash
curl "https://app.sumhour.com/api/v1/people" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### Add a client

`POST /api/v1/clients` · permission `projects:write`

Adds a client with its billing currency. Owners and admins only.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `name` | body | string | yes |  |
| `currency` | body | string |  | Three-letter code; defaults to the workspace currency |
| `billingEmail` | body | string or null |  |  |

```bash
curl -X POST "https://app.sumhour.com/api/v1/clients" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"…"}'
```

### Add a project

`POST /api/v1/projects` · permission `projects:write`

Adds an hourly, fixed-fee or non-billable project for a client. Owners and admins only; retainers and budgets are set up in the app.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `clientId` | body | id | yes |  |
| `name` | body | string | yes |  |
| `code` | body | string |  |  |
| `type` | body | `hourly`, `fixed_fee`, `non_billable` |  | default "hourly" |
| `hourlyRate` | body | string |  | One rate for the whole project, hourly projects |
| `fixedFee` | body | string |  | The fee, fixed-fee projects |

```bash
curl -X POST "https://app.sumhour.com/api/v1/projects" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"clientId":"<clientId>","name":"…"}'
```

## Time

### Running timer

`GET /api/v1/timer` · permission `time:read`

The caller's running timer, or null when nothing runs.

No parameters.

```bash
curl "https://app.sumhour.com/api/v1/timer" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### Start a timer

`POST /api/v1/timer/start` · permission `time:write`

Start the caller's timer on a project (stopping any running one), or resume an existing entry from today by passing entryId.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `projectId` | body | id |  |  |
| `taskId` | body | id or null |  |  |
| `notes` | body | string or null |  |  |
| `entryId` | body | id |  | Resume this entry instead of starting a new one |
| `billable` | body | boolean |  | Override the project's billable default |
| `startedAt` | body | string |  | When the timer was started, for a start made offline (within the last 12 hours) |
| `source` | body | object |  |  |

```bash
curl -X POST "https://app.sumhour.com/api/v1/timer/start" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"notes":"Kickoff call"}'
```

### Stop the timer

`POST /api/v1/timer/stop` · permission `time:write`

Stop the caller's running timer. Returns the stopped entry, or null when nothing was running.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `entryId` | body | id |  | Stop only this entry; 409 timer_not_running if it already stopped elsewhere |
| `stoppedAt` | body | string |  | When it was stopped, for a stop made offline |

```bash
curl -X POST "https://app.sumhour.com/api/v1/timer/stop" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json"
```

### List time entries

`GET /api/v1/entries` · permission `time:read`

Time entries between two dates (default: this week), newest first. Managers and admins can pass userId for someone else, or userId=all.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `from` | query | date |  |  |
| `to` | query | date |  |  |
| `userId` | query | string |  | A person's id, or "all"; defaults to the caller |
| `projectId` | query | id |  |  |
| `limit` | query | integer |  | default 100 |
| `cursor` | query | string |  |  |

```bash
curl "https://app.sumhour.com/api/v1/entries" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### Log time

`POST /api/v1/entries` · permission `time:write`

Log a finished block of time for the caller. Give a duration like "1:30" or "1.5h" (or seconds). Workspace rules apply: required notes or tasks, locked weeks, project membership.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `projectId` | body | id | yes |  |
| `taskId` | body | id or null |  |  |
| `date` | body | date |  | Defaults to today in the caller's timezone |
| `duration` | body | string |  |  |
| `seconds` | body | integer |  |  |
| `notes` | body | string or null |  |  |
| `billable` | body | boolean |  | Defaults to the project's setting |

```bash
curl -X POST "https://app.sumhour.com/api/v1/entries" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"<projectId>","duration":"1:30","notes":"Kickoff call"}'
```

### Change a time entry

`PATCH /api/v1/entries/{entryId}` · permission `time:write`

Change an entry's project, task, date, duration, notes or billable flag. Only the fields you pass change. Locked or approved time cannot change.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `entryId` | path | id | yes |  |
| `projectId` | body | id |  |  |
| `taskId` | body | id or null |  |  |
| `date` | body | date |  |  |
| `duration` | body | string |  |  |
| `seconds` | body | integer |  |  |
| `notes` | body | string or null |  |  |
| `billable` | body | boolean |  |  |

```bash
curl -X PATCH "https://app.sumhour.com/api/v1/entries/<entryId>" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"duration":"1:30","notes":"Kickoff call"}'
```

### Delete a time entry

`DELETE /api/v1/entries/{entryId}` · permission `time:write` · deletes data

Delete one of the caller's entries (admins: anyone's). Locked or invoiced time cannot be deleted.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `entryId` | path | id | yes |  |

```bash
curl -X DELETE "https://app.sumhour.com/api/v1/entries/<entryId>" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### A week's timesheet

`GET /api/v1/timesheets/{weekStart}` · permission `time:read`

The caller's week: status (open, submitted, approved, rejected, locked), hours per day and the total. weekStart may be any date in the week.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `weekStart` | path | date | yes |  |

```bash
curl "https://app.sumhour.com/api/v1/timesheets/2026-09-21" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### Submit a week

`POST /api/v1/timesheets/{weekStart}/submit` · permission `time:write`

Submit the caller's week for approval. Stop any running timer first.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `weekStart` | path | date | yes |  |

```bash
curl -X POST "https://app.sumhour.com/api/v1/timesheets/2026-09-21/submit" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json"
```

## Expenses

### Expense categories

`GET /api/v1/expense-categories` · permission `expenses:read`

Expense categories, some with a fixed unit price (mileage and similar).

No parameters.

```bash
curl "https://app.sumhour.com/api/v1/expense-categories" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### List expenses

`GET /api/v1/expenses` · permission `expenses:read`

Expenses between two dates (default: this month). People see their own; owners and admins can pass userId, or leave it out for everyone.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `from` | query | date |  |  |
| `to` | query | date |  |  |
| `projectId` | query | id |  |  |
| `userId` | query | string |  |  |

```bash
curl "https://app.sumhour.com/api/v1/expenses" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### Add an expense

`POST /api/v1/expenses` · permission `expenses:write`

Add an expense for the caller on a project. Receipts are uploaded in the app. For a unit-priced category pass units instead of amount.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `projectId` | body | id | yes |  |
| `categoryId` | body | id or null |  |  |
| `date` | body | date |  |  |
| `amount` | body | string |  |  |
| `units` | body | string |  |  |
| `notes` | body | string or null |  |  |
| `billable` | body | boolean |  | default true |
| `reimbursable` | body | boolean |  | default false |

```bash
curl -X POST "https://app.sumhour.com/api/v1/expenses" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"<projectId>","amount":"42.00","notes":"Kickoff call"}'
```

## Invoices

### List invoices

`GET /api/v1/invoices` · permission `invoices:read`

Invoices, newest first, optionally by status (draft, sent, viewed, paid, void) or client.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `status` | query | `draft`, `sent`, `viewed`, `paid`, `void` |  |  |
| `clientId` | query | id |  |  |

```bash
curl "https://app.sumhour.com/api/v1/invoices" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### An invoice

`GET /api/v1/invoices/{invoiceId}` · permission `invoices:read`

One invoice with its lines and payments.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `invoiceId` | path | id | yes |  |

```bash
curl "https://app.sumhour.com/api/v1/invoices/<invoiceId>" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### Draft an invoice

`POST /api/v1/invoices` · permission `invoices:write`

Draft an invoice for a client from uninvoiced time (and expenses) up to a date. Nothing is sent; the draft opens in the app for review.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `clientId` | body | id | yes |  |
| `projectIds` | body | list of id |  | Default: all the client's projects |
| `from` | body | date |  |  |
| `to` | body | date |  | Default: today |
| `groupBy` | body | `task`, `person`, `date`, `project`, `entry` |  | default "task" |
| `includeExpenses` | body | boolean |  | default true |
| `showNotes` | body | boolean |  | Print each time entry's note on the invoice. Notes are often internal. |

```bash
curl -X POST "https://app.sumhour.com/api/v1/invoices" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"clientId":"<clientId>"}'
```

### Record a payment

`POST /api/v1/invoices/{invoiceId}/payments` · permission `invoices:write`

Record a payment against a sent invoice. The invoice turns paid when fully covered.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `invoiceId` | path | id | yes |  |
| `amount` | body | string | yes |  |
| `paidOn` | body | date |  |  |
| `method` | body | `manual`, `bank`, `card`, `other` |  | default "bank" |
| `reference` | body | string or null |  |  |
| `notes` | body | string or null |  |  |

```bash
curl -X POST "https://app.sumhour.com/api/v1/invoices/<invoiceId>/payments" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"amount":"42.00","notes":"Kickoff call"}'
```

## Reports

### Run a report

`GET /api/v1/reports/{kind}` · permission `reports:read`

Run a report: time, profitability, utilization, invoicing, uninvoiced. Pick a preset (this_month, last_month, this_quarter, last_quarter, this_year, last_year, last_30, last_90) or from/to. Managers see only the projects they manage.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `kind` | path | `time`, `profitability`, `utilization`, `invoicing`, `uninvoiced` | yes |  |
| `preset` | query | `this_month`, `last_month`, `this_quarter`, `last_quarter`, `this_year`, `last_year`, `last_30`, `last_90` |  |  |
| `from` | query | date |  |  |
| `to` | query | date |  |  |
| `groupBy` | query | string |  | client, project, task, person, date, week or month (depends on the report) |
| `clientId` | query | id |  |  |
| `projectId` | query | id |  |  |
| `userId` | query | string |  |  |
| `billable` | query | `all`, `billable`, `nonbillable` |  | default "all" |

```bash
curl "https://app.sumhour.com/api/v1/reports/time" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

## Approvals

### Weeks waiting for review

`GET /api/v1/approvals` · permission `approvals:read`

Submitted weeks waiting for approval, with hours, plus people who have not submitted last week.

No parameters.

```bash
curl "https://app.sumhour.com/api/v1/approvals" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```

### Approve a week

`POST /api/v1/approvals/{userId}/{weekStart}/approve` · permission `approvals:write`

Approve a person's submitted week (weekStart may be any date in the week). Approved time can be invoiced and no longer changes.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `userId` | path | string | yes |  |
| `weekStart` | path | date | yes |  |
| `comment` | body | string or null |  |  |

```bash
curl -X POST "https://app.sumhour.com/api/v1/approvals/…/2026-09-21/approve" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json"
```

### Send a week back

`POST /api/v1/approvals/{userId}/{weekStart}/reject` · permission `approvals:write`

Send a submitted week back to the person with a comment saying what to change (weekStart may be any date in the week).

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `userId` | path | string | yes |  |
| `weekStart` | path | date | yes |  |
| `comment` | body | string | yes |  |

```bash
curl -X POST "https://app.sumhour.com/api/v1/approvals/…/2026-09-21/reject" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"comment":"…"}'
```

## Other

### Send events to a URL

`POST /api/v1/events/subscriptions` · permission `events:write`

Subscribes a URL to one workspace event (the same events as webhooks), for apps such as Zapier. The URL gets a signed POST each time. Owners and admins only.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `url` | body | string | yes |  |
| `event` | body | string | yes | An event name, e.g. "invoice.send" |

```bash
curl -X POST "https://app.sumhour.com/api/v1/events/subscriptions" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"…","event":"…"}'
```

### Stop sending events to a URL

`DELETE /api/v1/events/subscriptions/{id}` · permission `events:write`

Removes an event subscription made with events.subscribe.

| Name | In | Type | Required | Notes |
|---|---|---|---|---|
| `id` | path | id | yes |  |

```bash
curl -X DELETE "https://app.sumhour.com/api/v1/events/subscriptions/<id>" \
  -H "Authorization: Bearer $SUMHOUR_TOKEN"
```
