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. 1. Sign in with Google at /login/.
  2. 2. Create an API key at /api-keys/ — pick Full access or Selected lists with read/write toggles.
  3. 3. Use the key in an X-API-Key header 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"}
CodeMeaning
400Bad request — usually a missing required field.
401Missing or invalid API key. code: "needs_reauth" means the user must re-link Google.
403API key lacks the required scope for the requested list (per-key ACL).
404List or task not found.
429Rate limit exceeded — see Rate limits.
502Upstream 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

GET /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.

POST /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": "..."
}
GET /tasklists/{list_id}/

Retrieve a single task list by id.

Response 200

{ "kind": "tasks#taskList", "id": "...", "title": "My Tasks", ... }
PATCH /tasklists/{list_id}/

Rename a task list. Requires write access.

Request body

{ "title": "Renamed list" }

Response 200

{ "kind": "tasks#taskList", "id": "...", "title": "Renamed list", ... }
DELETE /tasklists/{list_id}/

Delete a task list and every task inside it. Requires write access. Permanent.

Response 204

No body.

POST /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

GET /tasklists/{list_id}/tasks/

List tasks in a list.

Query parameters

  • show_completed — boolean, default true
  • show_hidden — boolean, default true
  • show_deleted — boolean, default false

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"
  },
  ...
]
POST /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 of
  • previous — 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"
}
GET /tasklists/{list_id}/tasks/{task_id}/

Get one task.

Response 200

{ "kind": "tasks#task", "id": "...", "title": "...", ... }
PATCH /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).

DELETE /tasklists/{list_id}/tasks/{task_id}/

Permanently delete a task. Requires write access.

Response 204

POST /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 Tasks
  • openid
  • https://www.googleapis.com/auth/userinfo.email
  • https://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.