REST API
Read and change a project's tasks, requests and questions from a script, a CI job or another app, with an API token. Every endpoint, with examples.
The REST API is for a program of your own: a script, a CI job, a cron job, or another app. It reads and changes a project's lists the way the MCP tools do, and answers in JSON. Agents don't need it; they use the MCP tools.
The address is https://hamloy.com/api/v1. Every request sends an API token from the Connect page:
curl https://hamloy.com/api/v1/projects \
-H "Authorization: Bearer $HAMLOY_TOKEN"
A token reaches every project you are on, with what you may do there. What a program changes is signed api in the history and in the agents' news ("The API ticked B-k3f9", or "Sara's api" on a shared project), so you can always tell what a program did from what you did yourself.
Projects
GET /projects lists the projects you are on:
{
"projects": [
{ "id": "4f0c2a9e-…", "name": "Pebble", "owner": "Sara", "yours": true, "read_only": null,
"board": "https://hamloy.com/p/4f0c2a9e-…" }
]
}
read_only says why, when you can read a project but not change it, such as a project your plan no longer covers.
GET /projects/:id is one project, with its lists:
{ "project": { "id": "4f0c2a9e-…", "name": "Pebble", …,
"lists": [ { "name": "backlog", "kind": "task", "prefix": "B" }, { "name": "requests", "kind": "request", "prefix": "R" },
{ "name": "questions", "kind": "question", "prefix": "Q" }, { "name": "ideas", "kind": "own", "prefix": "I" } ] } }
Listing items
GET /projects/:id/items lists what the list tool would, in list order, subtasks right after their parent:
| Parameter | What it does |
|---|---|
kind |
task, request, question, one of your own lists by name, or all (the default) |
status |
open (the default), ready (open and waiting on nothing), done or all |
section |
only items under this heading |
find |
items with all these words in their key, title, notes or fields; done ones too |
from, limit |
paging: 50 at a time by default, at most 200 |
curl "https://hamloy.com/api/v1/projects/$PROJECT/items?kind=task&status=ready" \
-H "Authorization: Bearer $HAMLOY_TOKEN"
{
"project": { "id": "4f0c2a9e-…", "name": "Pebble" },
"total": 12, "from": 0, "next": null,
"items": [
{
"key": "B-k3f9", "version": "B-k3f9@3", "kind": "task", "list": "backlog",
"title": "Streak counter on the habit card", "summary": null, "section": "This week",
"notes": "Count days in a row, not days in total.", "fields": { "area": "app" },
"parent": null, "subtasks": [], "needs": [], "ready": true, "claimed_by": "claude",
"done": false, "done_at": null, "done_by": null,
"added_at": "2026-10-17T09:12:44Z", "added_by": "claude",
"url": "https://hamloy.com/p/4f0c2a9e-…/items/B-k3f9"
}
]
}
next is the from of the next page, or null on the last. Every item has the same keys, null where it has nothing. A question also has options (each with text, recommended and assumed), answer and why.
GET /projects/:id/items/:key is one item. An old key still finds an item that moved to another list.
Adding an item
POST /projects/:id/items with a JSON body:
curl https://hamloy.com/api/v1/projects/$PROJECT/items \
-H "Authorization: Bearer $HAMLOY_TOKEN" -H "Content-Type: application/json" \
-d '{"title": "Checkout fails for cards from Brazil", "kind": "task", "section": "Bugs",
"summary": "Stripe declines with a currency error", "notes": "Seen 14 times since Monday.",
"fields": {"area": "billing", "prio": "high"}}'
| Field | |
|---|---|
title |
required, one line |
kind |
task (the default), request, question or one of your own lists |
section |
the heading to put it under; made if missing |
summary |
one short line; required when you give notes |
notes |
text, one line per note |
fields |
names and text, such as {"prio": "high"} |
needs |
keys it waits on |
parent |
a task's key, to add this as its subtask |
subtasks |
titles of subtasks to add under it |
options, recommended |
a question's options, and the number of the one you recommend |
It answers 201 with the item.
Changing an item
PATCH /projects/:id/items/:key with any of title, summary, section, fields ("" takes a field away), needs (all of them; [] for none), notes (replaces them all), add_note, list (moves it), options, recommended, before and after (a key in the same list).
Send version as you read it ("B-k3f9@3") and a change someone else made since is never overwritten: the request is refused, and you read the item again.
curl -X PATCH https://hamloy.com/api/v1/projects/$PROJECT/items/B-k3f9 \
-H "Authorization: Bearer $HAMLOY_TOKEN" -H "Content-Type: application/json" \
-d '{"version": "B-k3f9@3", "fields": {"prio": "high"}, "add_note": "Two more reports today."}'
Ticking, answering, deleting
POST /projects/:id/items/:key/tick, with an optionalnote: marks a task or request done.{"done": false}opens it again.POST /projects/:id/items/:key/answer, withanswer(an option's number or its words, or your own) and an optionalwhy. Answering is yours, as on the board. The reply'sready_nowlists what no longer waits on it.DELETE /projects/:id/items/:keydeletes it, with its subtasks.no_longer_waitinglists what waited on it.POST /projects/:id/items/:key/restorebrings it back.
Each answers with the item as it is now.
Errors
A refused request answers with a status and one sentence in error:
{ "error": "B-k3f9 changed since you read it; it's now B-k3f9@4: Streak counter on the habit card. Read it again and redo your change if it still applies" }
| Status | When |
|---|---|
400 |
the body isn't JSON |
401 |
no token, or one that isn't, or was revoked |
403 |
the project is read-only for you, and error says why |
404 |
no such project, or it isn't open to you |
422 |
the request can't be done as asked: an unknown key, a stale version, a missing title |
429 |
more than 100 requests in a minute with one token |
Limits
- 100 requests a minute for each token.
- No claims: a program doesn't hold a task the way an agent does. To have work picked up, add it, and the agents take it from the lists.
- To send work in from a service that posts its own JSON, such as an error tracker or a form, without writing code, use a project's webhook instead.