REST API
Base URL https://app.sumhour.com/api/v1. JSON in, JSON out, camelCase names. The full machine-readable description is 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 }; passcursorback 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-Idheader. 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 (headerIdempotent-Replayed: true) instead of doing it twice; the same key with a different body is422. Keys are kept 24 hours. - 120 requests a minute per token. Over that you get
429withRetry-After, plusRateLimit-Limit,RateLimit-RemainingandRateLimit-Reseton every response. - Durations accept
1:30,1.5h,90morseconds. Dates areYYYY-MM-DDin 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.
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 |
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 |
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.
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.
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 |
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 |
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.
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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.
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 |
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 |
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 |
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 |
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. |
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 |
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" |
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.
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 |
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 |
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" |
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 |
curl -X DELETE "https://app.sumhour.com/api/v1/events/subscriptions/<id>" \
-H "Authorization: Bearer $SUMHOUR_TOKEN"