HES gTasks — Docs
Multi-tenant Google Tasks proxy with an HTTP API, MCP connector for Claude Code, recurring tasks, and per-key list ACL.
Quick start
- 1. Sign in with Google at /login/.
- 2. Create an API key at /api-keys/ — pick Full access or Selected lists with read/write toggles.
- 3. Use the key in an
X-API-Keyheader on every call.
Authentication
Every endpoint requires an API key sent in one of two ways:
X-API-Key: hes_AbCdEf...
— or —
Authorization: Bearer hes_AbCdEf...
Keys begin with hes_. The raw key is shown once at creation time; we store only a SHA-256 hash thereafter.
Errors
All errors return JSON in DRF's default shape:
{"detail": "Human-readable explanation"}
| Code | Meaning |
|---|---|
| 400 | Bad request — usually a missing required field. |
| 401 | Missing or invalid API key. code: "needs_reauth" means the user must re-link Google. |
| 403 | API key lacks the required scope for the requested list (per-key ACL). |
| 404 | List or task not found. |
| 429 | Rate limit exceeded — see Rate limits. |
| 502 | Upstream Google Tasks API error. Retry with backoff. |
Resources
TaskList
{
"kind": "tasks#taskList",
"id": "MDczMjMxOTYx...", // string, opaque, Google-issued
"etag": "\"PHgZba4TDTE\"",
"title": "My Tasks", // string, the displayed name
"updated": "2026-05-13T16:50:01.366Z",
"selfLink": "https://www.googleapis.com/tasks/v1/users/@me/lists/..."
}
Task
{
"kind": "tasks#task",
"id": "RmNVR29aOEp4...", // string, opaque
"etag": "\"abc123\"",
"title": "Buy milk", // string, required for create
"status": "needsAction", // "needsAction" | "completed"
"due": "2026-06-01T00:00:00.000Z", // RFC3339, date-only (time portion ignored by Google)
"notes": "2% organic", // string, optional
"completed": "2026-06-02T14:00:00Z", // RFC3339, only set when status="completed"
"parent": "AbCdEf...", // present if this is a subtask
"position": "00000000003221225472",
"updated": "2026-05-13T16:00:00Z",
"hidden": false,
"deleted": false,
"links": [{ "type": "email", "description": "...", "link": "https://..." }],
"selfLink": "..."
}
Endpoints
Base URL: http://gtasks.henry.enterprises/api/v1
Task lists
/tasklists/
List all task lists the API key has read access to.
Response 200
[
{
"kind": "tasks#taskList",
"id": "MDczMjMxOTYxNjAxMDc5MzI3NTc6MDow",
"title": "My Tasks",
"updated": "2026-05-10T16:49:31.366Z",
"etag": "...", "selfLink": "..."
},
...
]
Scoped keys see only lists they were granted read access to.
/tasklists/
Create a new task list. Full-access keys only; scoped keys return 403.
Request body
{ "title": "Groceries" }
Response 201
{
"kind": "tasks#taskList",
"id": "AbCdEf...",
"title": "Groceries",
"updated": "2026-05-13T17:00:00.000Z",
"selfLink": "..."
}
/tasklists/{list_id}/
Retrieve a single task list by id.
Response 200
{ "kind": "tasks#taskList", "id": "...", "title": "My Tasks", ... }
/tasklists/{list_id}/
Rename a task list. Requires write access.
Request body
{ "title": "Renamed list" }
Response 200
{ "kind": "tasks#taskList", "id": "...", "title": "Renamed list", ... }
/tasklists/{list_id}/
Delete a task list and every task inside it. Requires write access. Permanent.
Response 204
No body.
/tasklists/{list_id}/clear/
Hide every completed task in this list (Google's idiomatic "clear completed"). Tasks aren't deleted from Google, just hidden from the default view.
Response 204
Tasks
/tasklists/{list_id}/tasks/
List tasks in a list.
Query parameters
show_completed— boolean, defaulttrueshow_hidden— boolean, defaulttrueshow_deleted— boolean, defaultfalse
Response 200
[
{
"kind": "tasks#task",
"id": "RmNVR29aOEp4dUtNWXg2Ug",
"title": "Checkout Rust Skins",
"status": "needsAction",
"due": "2026-05-07T00:00:00.000Z",
"notes": "https://store.steampowered.com/...",
"position": "00000000003221225472",
"updated": "2026-05-07T06:04:38.354Z"
},
...
]
/tasklists/{list_id}/tasks/
Create a task. Requires write access.
Request body
{
"title": "Buy milk", // required
"notes": "2% organic", // optional
"due": "2026-06-01T00:00:00.000Z", // optional, RFC3339
"status": "needsAction" // optional, default "needsAction"
}
Query parameters (optional)
parent— task id to make this a subtask ofprevious— task id to position this immediately after
Response 201
{
"kind": "tasks#task",
"id": "NewTaskAbCdEf...",
"title": "Buy milk",
"status": "needsAction",
"due": "2026-06-01T00:00:00.000Z",
"notes": "2% organic",
"position": "00000000004294967296",
"updated": "2026-05-13T17:00:00.000Z"
}
/tasklists/{list_id}/tasks/{task_id}/
Get one task.
Response 200
{ "kind": "tasks#task", "id": "...", "title": "...", ... }
/tasklists/{list_id}/tasks/{task_id}/
Partially update a task. Only the supplied fields change.
Request body (any subset)
{
"title": "Updated title",
"notes": "New notes",
"due": "2026-07-01T00:00:00.000Z",
"status": "completed" // setting "completed" auto-fills the completed timestamp
}
Response 200
{ "kind": "tasks#task", ...updated task... }
Note: setting status: "completed" on a task that has an on-complete recurrence rule will spawn the next instance automatically (web UI only — recurrence does not currently auto-fire via API).
/tasklists/{list_id}/tasks/{task_id}/
Permanently delete a task. Requires write access.
Response 204
/tasklists/{list_id}/tasks/{task_id}/move/
Reorder a task within a list and/or reparent it. To move to top, pass an empty/missing previous.
Request body (all optional)
{
"parent": "AnotherTaskId", // null/omit = top level
"previous": "TaskBeforeId" // null/omit = first in list
}
Response 200
{ "kind": "tasks#task", "id": "...", "position": "new-position", ... }
Google's API doesn't support cross-list moves natively. To move between lists, create the task in the destination list and delete the original.
End-to-end example
# 1. List your task lists
curl -s -H "X-API-Key: hes_..." \
http://gtasks.henry.enterprises/api/v1/tasklists/
# 2. Create a task in the first list
LIST_ID=MDczMjMxOTYx...
curl -s -X POST \
-H "X-API-Key: hes_..." \
-H "Content-Type: application/json" \
-d '{"title": "Buy milk", "due": "2026-06-01T00:00:00.000Z"}' \
http://gtasks.henry.enterprises/api/v1/tasklists/$LIST_ID/tasks/
# 3. Mark it completed
TASK_ID=NewTaskAbC...
curl -s -X PATCH \
-H "X-API-Key: hes_..." \
-H "Content-Type: application/json" \
-d '{"status": "completed"}' \
http://gtasks.henry.enterprises/api/v1/tasklists/$LIST_ID/tasks/$TASK_ID/
# 4. Delete it
curl -s -X DELETE \
-H "X-API-Key: hes_..." \
http://gtasks.henry.enterprises/api/v1/tasklists/$LIST_ID/tasks/$TASK_ID/
Rate limits
60 requests per minute per API key. Exceeding returns 429 Too Many Requests with a Retry-After hint in seconds.
Per-key list ACL
When you create an API key on the API Keys page, choose:
- Full access — read/write across every list you own; can also create new top-level lists.
- Selected lists — per-list Read and Write toggles. Write implies Read.
Scoped keys can only see the lists they were granted; GET /tasklists/ is filtered server-side. Writes to ungranted lists return 403. Scoped keys cannot create new top-level lists.
Recurring tasks
Google's public Tasks API has no recurrence field, so we own it locally. From a task's detail modal in the web UI, pick Repeat and choose Daily/Weekly/Monthly/Yearly with an interval. Two trigger modes:
- After completing — next instance spawns when you mark the current one complete.
- On a schedule — a background worker polls every 10 min and spawns due instances regardless of completion state.
Recurrence configuration is currently only exposed via the web UI (not the REST API). Tracking the issue.
Claude Code MCP connector
A Python MCP server is bundled in hes-gtasks/mcp/:
pip install -r mcp/requirements.txt
claude mcp add gtasks python /path/to/hes-gtasks/mcp/server.py \
--env GTASKS_API_URL=http://gtasks.henry.enterprises/api/v1 \
--env GTASKS_API_KEY=hes_...
Tools exposed (each maps 1:1 to a REST endpoint above): list_tasklists, get_tasklist, create_tasklist, update_tasklist, delete_tasklist, list_tasks, get_task, create_task, update_task, delete_task, move_task, clear_completed.
All ACL rules apply automatically: a scoped key only exposes accessible lists to the AI agent.
OAuth scopes we request from you
https://www.googleapis.com/auth/tasks— read/write Google Tasksopenidhttps://www.googleapis.com/auth/userinfo.emailhttps://www.googleapis.com/auth/userinfo.profile
Refresh tokens are stored encrypted at rest (Fernet AES-128). API keys are stored as SHA-256 hashes; the raw value is shown once at creation.