Tuhmo API

Read and write tasks, notes, clients and projects in Tuhmo from your own systems, and get a webhook when something changes.

Getting started

Create a key in Settings, then Developer, under API keys. The key is shown only once, so keep it somewhere safe.

Send the key in the Authorization header of every request, then call /me to check that it works.

curl https://app.tuhmo.com/api/v1/me \
  -H "Authorization: Bearer tmk_..."

What a key can do

A key acts as the person who created it, in the workspace it was created in. It sees and changes only what that person can. Other people's private pages stay private. If the person leaves the workspace, the key stops working right away.

Keys are either read only or read and write. Free workspaces get 2 read only keys per person. Plus and above get 10 keys per person that can also write. Money documents are read only on every plan, and the API has no way to send documents to clients.

Endpoints

Every path starts with https://app.tuhmo.com/api/v1. {id} is the 24 character id of the item. Requests and responses are JSON.

MethodPathWhat it does
GET/meWho owns this key, which workspace, and whether it can write
GET/tasksList tasks
GET/tasks/{id}Get a task
POST/tasksCreate a task (title and status are required)
PATCH/tasks/{id}Update some fields of a task
DELETE/tasks/{id}Move a task to the trash
PATCH/tasks/{id}/propsUpdate custom properties
GET/tasks/{id}/commentsComments on a task
POST/tasks/{id}/commentsComment on a task
GET/tasks/{id}/githubGitHub pull requests, issues and commits linked to a task
GET/tasksetsTask sets
GET/notes/treeTree of every note you can see
GET/notes/search?q=Search notes
GET/notes/{id}Get a note with its content
POST/notesCreate a note
PATCH/notes/{id}Update a note's title or content
DELETE/notes/{id}Move a note to the trash
GET/notes/{id}/commentsComments on a note
POST/notes/{id}/commentsComment on a note
GET/clientsList clients
GET/clients/{id}Get a client
POST/clientsCreate a client
PUT/clients/{id}Update a client
GET/clients/{id}/jobsA client's projects
GET/projectsList projects
GET/projects/job/{id}Get a project
POST/jobsCreate a project
PATCH/jobs/{id}Update a project
GET/invoices · /invoices/{id}Invoices (read only)
GET/proposals · /proposals/{id}Quotes (read only)
GET/contracts · /contracts/{id}Contracts (read only)

Example: create a task

curl -X POST https://app.tuhmo.com/api/v1/tasks \
  -H "Authorization: Bearer tmk_..." \
  -H "Content-Type: application/json" \
  -d '{"title": "Send files to the client", "status": "todo", "dueDate": "2026-10-20"}'

status is one of todo, doing, review or done. dueDate is YYYY-MM-DD.

Every task has a number, its short ID in the workspace (42 in TUH-42). Put it in a commit or PR and the task links to GitHub by itself. Linked PRs are at /tasks/{id}/github.

Webhooks

Owners and admins add destinations in Settings, then Developers, and pick the events they want. Every event is a JSON POST to the destination URL.

Note content is never sent. Call /notes/{id} with an API key when you need it. Private pages and anything under them are never sent.

eventSent whenWhat data has
task.createdA task is createdThe whole task
task.updatedA task changes, such as title, status, dates or assigneeThe whole task
task.status_changedA task's status changes (sent along with task.updated)The task, plus previousStatus
task.deletedA task is moved to the trashid, title, number, url
note.createdA note page is createdid, title, icon, parentId, projectId, clientId, timestamps, url (no content)
note.updatedA note changes, sent once editing stops for 30 secondsSame as note.created
note.deletedA note is moved to the trashid, title, url
comment.createdA new comment on a task, note or table rowThe comment, plus the url of where it is
pingYou press Test in settingsNo data
{
  "id": "6720c1f2a9e4b3d1c0a1b2c3",
  "event": "task.status_changed",
  "at": "2026-10-20T09:30:00Z",
  "workspaceId": "66f0a8e2c4b1d2e3f4a5b6c7",
  "data": {
    "id": "6714e0b2a9e4b3d1c0a19f00",
    "previousStatus": "doing",
    "status": "review",
    "number": 42,
    "title": "...",
    "dueDate": "2026-10-20T00:00:00Z",
    "url": "https://app.tuhmo.com/tasks?task=6714e0b2a9e4b3d1c0a19f00"
  }
}

The top level id identifies the delivery and stays the same on retries, so you can use it to ignore duplicates. at is when the event happened. data.id is the id of the task, note or comment.

Signatures and retries

Every request carries X-Event (the event name), X-Delivery-Id (same as id in the body) and X-Signature-256: an HMAC SHA256 of the raw body, signed with the destination's whsec_ secret. Check it before trusting the data.

import crypto from "node:crypto";

// body = the raw request body (before JSON.parse), secret = the destination's whsec_ secret
function verify(body, header, secret) {
  const sig = "sha256=" + crypto.createHmac("sha256", secret).update(body).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(header ?? ""));
}

Respond with a 2xx within 15 seconds. Otherwise we retry 5 more times over about 10 hours. After 20 failures in a row the destination is turned off and owners and admins are notified.

Free workspaces get 1 destination and 100 deliveries per hour. Plus and above get 10 destinations and 1,000 deliveries per hour each.

Incoming webhooks

Let other systems send data into Tuhmo through a single URL, with no key. Good for GitHub Actions, Zapier or forms. Create a URL in Settings, Developers, under Incoming webhooks (owners and admins, Plus and above).

The URL acts as the person who created it and can see and change only what they can. It is shown once, and anyone with it can send data in. If it leaks, create a new URL and the old one stops working right away.

Send a JSON POST with one of three actions. Incoming webhooks never touch money documents and cannot send anything to clients.

actionFieldsWhat it does
create_tasktitle note status dueDate projectId clientIdCreates a task. status defaults to todo
append_to_notenoteId textAppends text to the end of a note
add_commenttaskId or noteId, and textComments on a task or note
curl -X POST https://app.tuhmo.com/api/hooks/in/tih_... \
  -H "Content-Type: application/json" \
  -d '{"action": "create_task", "title": "Build failed on main", "dueDate": "2026-10-20"}'

Success returns 201 or 200 with the id of what was created. A turned off URL returns 403 with code disabled. Up to 60 requests per minute per URL, with bodies up to 256 KB.

Every request is kept for 14 days in the URL's history: what it did, whether it worked, and which IP it came from.

Limits

On average 3 requests per second per key, with bursts up to 10, and no more than 10 requests per second per workspace. Over that you get a 429 with a Retry-After header in seconds.

Request bodies are limited to 1 MB.

Errors

Errors are JSON with a human readable error and a code your program can branch on.

StatuscodeMeaning
401unauthorizedMissing, wrong, expired or revoked key
403read_onlyA read only key tried to change data
403planThe workspace plan does not include this, such as writing on Free
403The key's owner is not allowed to do this in the workspace
403disabledThe incoming webhook URL is turned off
404not_foundThe path is not part of the API, or the item does not exist
413too_largeRequest body over 1 MB (256 KB for incoming webhooks)
429rate_limitedToo many requests, wait for Retry-After