# Build on Sumhour Four ways in. Each acts as one person inside one workspace, never beyond that person's role, and only within the permissions they granted. | You want to | Use | |---|---| | Let Claude, ChatGPT or Cursor log time, read reports and draft invoices | [MCP](/developers/mcp) | | Script Sumhour from any language | [REST API](/developers/api) | | Stay in the terminal | [Command line](/developers/cli) | | React when something happens in Sumhour | [Webhooks](/developers/webhooks) | ## Quick start 1. In Sumhour, open your account menu, then **API tokens**, and make a token. Copy it; it is shown once. 2. Call the API: ```bash export SUMHOUR_TOKEN=sh_pat_... curl https://app.sumhour.com/api/v1/me -H "Authorization: Bearer $SUMHOUR_TOKEN" ``` 3. Log an hour and a half: ```bash curl -X POST https://app.sumhour.com/api/v1/entries \ -H "Authorization: Bearer $SUMHOUR_TOKEN" -H "Content-Type: application/json" \ -d '{"projectId":"","duration":"1:30","notes":"Design review"}' ``` ## Permissions A token or an AI connection gets some of these. Presets on the tokens page: **Time tracking**, **Read only**, **Everything your role allows**. | Permission | Lets it | |---|---| | `time:read` | See entries, the timer and timesheets | | `time:write` | Log, change and delete time, run the timer, submit weeks | | `projects:read` | See projects, tasks and clients | | `projects:write` | Add clients and projects (admins and owners) | | `expenses:read` | See expenses | | `expenses:write` | Add expenses | | `invoices:read` | See invoices | | `invoices:write` | Draft invoices and record payments | | `reports:read` | Run reports | | `approvals:read` | See weeks waiting for review | | `approvals:write` | Approve and send back weeks | | `team:read` | See the people in the workspace | | `events:write` | Send workspace events to apps such as Zapier (admins and owners) | ## Rules that always apply - The workspace's own rules hold everywhere: locked and approved weeks do not change, required notes and tasks are required, contractors see only their projects. - Every change is recorded in the Activity log with where it came from, for example *via API · Zapier* or *via MCP · Claude*. - Removing someone from the workspace, or revoking a token or a connection, takes effect on the next request. ## This server Everything on these pages points at **https://app.sumhour.com**. A self-hosted Sumhour serves the same pages with its own address. - OpenAPI 3.1: [https://app.sumhour.com/api/v1/openapi.json](https://app.sumhour.com/api/v1/openapi.json) - MCP endpoint: `https://app.sumhour.com/mcp` - For AI agents: [llms.txt](https://app.sumhour.com/llms.txt) and [llms-full.txt](https://app.sumhour.com/llms-full.txt) --- # Connect an AI assistant Sumhour is an MCP server. Point any MCP client at ``` https://app.sumhour.com/mcp ``` The first time, the client opens Sumhour in the browser: you sign in, pick the workspace and untick anything you do not want to share. The assistant then acts as you, never beyond your role, and every change it makes is logged as *via MCP* with the client's name. Disconnect it any time under **API tokens → Connected apps**. ## Claude (claude.ai and the desktop app) **Customize → Connectors**, then **+** → **Add custom connector**, paste the URL above and **Add**. Leave the OAuth fields under Advanced settings empty. Works on every plan; the Free plan allows one custom connector. On Team and Enterprise an owner adds it for the organization first. ## Claude Code ```bash claude mcp add --transport http sumhour https://app.sumhour.com/mcp ``` Then run `/mcp` in a session and choose **Authenticate**. Without a browser (CI, a remote box), use an API token instead: ```bash claude mcp add --transport http sumhour https://app.sumhour.com/mcp --header "Authorization: Bearer sh_pat_..." ``` The Sumhour plugin for Claude Code bundles this connection with a skill that logs a coding session as time entries (`/sumhour:log-session`). It installs from the Sumhour repository once it is public. ## ChatGPT Settings → **Security and login** → turn on **Developer mode**. Then open [ChatGPT Plugins](https://chatgpt.com/plugins), press **+** and connect the URL above. ## Cursor [Add Sumhour to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=sumhour&config=eyJ1cmwiOiJodHRwczovL2FwcC5zdW1ob3VyLmNvbS9tY3AifQ==), or put this in `.cursor/mcp.json`: ```json { "mcpServers": { "sumhour": { "url": "https://app.sumhour.com/mcp" } } } ``` ## VS Code `.vscode/mcp.json`: ```json { "servers": { "sumhour": { "type": "http", "url": "https://app.sumhour.com/mcp" } } } ``` ## Tools The assistant sees only the tools its permissions and your role allow. Each tool is marked as reading, changing or deleting, so clients can ask before the risky ones. | Tool | What it does | Kind | Permission | |---|---|---|---| | `me_get` | Who am I | reads | | | `projects_list` | List projects | reads | `projects:read` | | `projects_tasks` | A project's tasks | reads | `projects:read` | | `clients_list` | List clients | reads | `projects:read` | | `timer_get` | Running timer | reads | `time:read` | | `timer_start` | Start a timer | changes | `time:write` | | `timer_stop` | Stop the timer | changes | `time:write` | | `entries_list` | List time entries | reads | `time:read` | | `entries_create` | Log time | changes | `time:write` | | `entries_update` | Change a time entry | changes | `time:write` | | `entries_delete` | Delete a time entry | deletes | `time:write` | | `timesheets_get` | A week's timesheet | reads | `time:read` | | `timesheets_submit` | Submit a week | changes | `time:write` | | `expenses_categories` | Expense categories | reads | `expenses:read` | | `expenses_list` | List expenses | reads | `expenses:read` | | `expenses_create` | Add an expense | changes | `expenses:write` | | `invoices_list` | List invoices | reads | `invoices:read` | | `invoices_get` | An invoice | reads | `invoices:read` | | `invoices_create_draft` | Draft an invoice | changes | `invoices:write` | | `invoices_record_payment` | Record a payment | changes | `invoices:write` | | `reports_run` | Run a report | reads | `reports:read` | | `approvals_list` | Weeks waiting for review | reads | `approvals:read` | | `approvals_approve` | Approve a week | changes | `approvals:write` | | `approvals_reject` | Send a week back | changes | `approvals:write` | | `people_list` | List people | reads | `team:read` | | `clients_create` | Add a client | changes | `projects:write` | | `projects_create` | Add a project | changes | `projects:write` | ## Prompts - **Log my day**: paste a rough note of your day; the assistant proposes entries and logs them after you confirm. - **Check my week**: finds short days, entries without notes and running timers before you submit. - **Month end** (accountants and admins): what is uninvoiced, what is overdue and which drafts to create. ## How sign-in works OAuth 2.1 with Sumhour as the authorization server. For client developers: - Protected resource metadata: `https://app.sumhour.com/.well-known/oauth-protected-resource/mcp` (an unauthenticated call to `/mcp` answers 401 pointing there). - Authorization server metadata: `https://app.sumhour.com/.well-known/oauth-authorization-server/api/auth`, issuer `https://app.sumhour.com/api/auth`. - Clients identify with a Client ID Metadata Document URL, or register dynamically. PKCE is required. Access tokens are audience-bound JWTs for an hour; refresh tokens come with `offline_access`. - Scopes are the permissions on the [overview](/developers#permissions). A client that asks for none is offered all of them, and the person can untick any. --- # 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: ` 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//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":"","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":"","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/" \ -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/" \ -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":"","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/" \ -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":""}' ``` ### 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//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/" \ -H "Authorization: Bearer $SUMHOUR_TOKEN" ``` --- # The sumhour command line Time tracking without leaving the terminal, built on the REST API. It needs Node 24 and nothing else. ## Sign in ```bash sumhour login ``` The terminal shows a code like `WDJB-MJHT` and opens https://app.sumhour.com/device. Check the code matches, pick the workspace and what the CLI may do, and approve. The CLI keeps a token that expires in 180 days and shows up under **API tokens**; `sumhour logout` revokes it. For CI and agents, skip the browser: set `SUMHOUR_TOKEN` (and `SUMHOUR_URL` for a self-hosted server). ## Everyday use ```bash sumhour start acme portal -m "Kickoff" # words match client, project or code sumhour stop sumhour log 1:30 AH-2 -m "Design review" -t design sumhour entries --today sumhour week && sumhour submit ``` ## Account ``` sumhour login [--url URL] [--token sh_pat_…] Sign in through the browser (or save a token) sumhour logout Revoke this device's token and forget it sumhour whoami Who you are signed in as, and where ``` ## Time ``` sumhour status Running timer and today's total sumhour start [project] [-t task] [-m notes] Start a timer (no project: resume today's last entry) sumhour stop Stop the running timer sumhour log [-m] [-t] [-d date] [--non-billable] Log finished time (1:30, 1.5h, 90m) sumhour entries [--today|--yesterday|--from D --to D] [--all] [-p project] List time (default this week) sumhour edit [--duration] [-m] [-d] [-p] [--billable|--non-billable] Change an entry sumhour rm [--yes] Delete an entry sumhour week [date] A week's timesheet and status sumhour submit [date] Submit the week for approval ``` ## Workspace ``` sumhour projects [search] Projects you can log to, with codes and tasks sumhour clients Active clients sumhour people Everyone in the workspace ``` ## Money ``` sumhour expense [-m] [-c category] [--reimbursable] Add an expense sumhour invoices [--status sent] Invoices (accountants and admins) sumhour report [time|profitability|utilization|invoicing|uninvoiced] [--preset this_month] [--group project] sumhour approvals Weeks waiting for review (managers) ``` ## Other ``` sumhour api [--data JSON|@file] Any /api/v1 call, JSON out ``` ## For scripts and agents - `--json` on every command; lists come back as plain arrays. - `--dry-run` on every write prints the exact request and sends nothing. - Deleting asks first at a terminal and needs `--yes` elsewhere. - A project name that matches several projects is an error that lists them, never a guess. - `sumhour api GET /projects` reaches any [API endpoint](/developers/api). | Exit code | Meaning | |---|---| | 0 | Done | | 2 | Usage problem, or an ambiguous project | | 3 | Not signed in, token revoked or expired | | 4 | Refused by the server (validation, permissions, locked time) | | 5 | Server or network failure | ## Install The npm package ships with the public launch. Until then, from a clone of the repository: `pnpm sumhour `. --- # Webhooks Sumhour posts a signed JSON event to your URL when something happens in a workspace. Admins set them up under **Settings → Integrations → Webhooks**. ## The request ``` POST https://your-server.example/hooks/sumhour content-type: application/json user-agent: Sumhour-Webhooks/1.0 (+https://app.sumhour.com/developers/webhooks) webhook-id: msg_9f1c… webhook-timestamp: 1758700000 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= ``` ```json { "id": "evt_9f1c…", "type": "invoice.send", "timestamp": "2026-09-24T09:17:09.120Z", "workspace": { "id": "…", "name": "Pixel Studio", "slug": "pixel-studio" }, "actor": { "id": "…", "name": "Habib", "type": "user", "via": "web", "client": null }, "summary": "Invoice sent: #1042 for Larkspur Health (USD 4,800.00)", "url": "https://app.sumhour.com/app/invoices/…", "data": { "entity": "invoice", "entityId": "…", "invoice": { "number": "1042", "status": "sent", "total": "4800.00", "currency": "USD", "client": { "name": "Larkspur Health" } }, "before": null, "after": { "number": "1042" } } } ``` Subscribe an endpoint to exact events, a prefix such as `invoice.*`, or everything with `*`. **Time**: `time_entry.create`, `time_entry.update`, `time_entry.delete`, `time_entry.bulk_update`, `timer.start`, `timer.stop`, `suggestion.accept` **Timesheets**: `timesheet.submit`, `timesheet.auto_submit`, `timesheet.withdraw`, `timesheet.approve`, `timesheet.reject`, `timesheet.reopen`, `timesheet.lock`, `timesheet.lock_through` **Expenses**: `expense.create`, `expense.update`, `expense.delete`, `expense.reimburse` **Invoices**: `invoice.create`, `invoice.update`, `invoice.send`, `invoice.payment`, `invoice.payment_delete`, `invoice.void`, `invoice.delete`, `invoice.push`, `recurring.create` **Estimates**: `estimate.create`, `estimate.send`, `estimate.to_invoice`, `estimate.to_project`, `estimate.delete` **Projects and clients**: `project.create`, `project.update`, `project.member_add`, `project.rate_change`, `project.progress`, `client.create`, `client.update` **Month end and workspace**: `monthend.write_off`, `monthend.write_down`, `member.role_change`, `member.remove`, `import.start`, `import.revert`, `webhook.test` ## Verify the signature Sumhour follows [Standard Webhooks](https://www.standardwebhooks.com/) 1.0, so any of its libraries verifies these requests. By hand: the secret is `whsec_` plus base64; sign `${webhook-id}.${webhook-timestamp}.${raw body}` with HMAC-SHA256, base64 it, prefix `v1,`, compare in constant time, and reject timestamps more than five minutes old. ```ts import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(secret: string, h: Record, rawBody: string) { const id = h["webhook-id"], ts = h["webhook-timestamp"], sig = h["webhook-signature"]; if (!id || !ts || !sig || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64"); return sig.split(" ").some((s) => { const given = s.replace(/^v1,/, ""); return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected)); }); } ``` ## Delivery and retries - Answer with any 2xx within 10 seconds. The body is ignored. - Anything else is retried 7 times: after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours. Then the delivery is marked as given up; retry it by hand from the delivery log. - `webhook-id` stays the same across retries; use it to ignore duplicates. - After 20 failures in a row the endpoint pauses itself. Fix the receiver and turn it back on. - Order is not guaranteed; trust `timestamp` and the entity's current state. ## Slack For a team channel rather than your own code, paste a Slack incoming webhook URL into the Slack card on the same Integrations page and pick which events to post.