Developers

API Reference

A small REST API to read and write your Gumhop tasks — built for scripts, integrations, and AI agents.

Base URLhttps://www.gumhop.com/api/v1

Authentication

Create an API key in Settings → API. Pass it as a Bearer token on every request. Keys are shown once — store them safely.

curl https://www.gumhop.com/api/v1/me \
  -H "Authorization: Bearer gh_live_…"

Errors & rate limits

Errors return a JSON body { "error": "message" } with an appropriate status: 401 (invalid or missing key), 400 (bad input), 404 (not found), 429 (rate limit exceeded).

The API is capped at 120 requests per 60-second window per account(shared across all of that account's keys). Requests over the cap are rejected with 429 { "error": "rate limit exceeded" } — retry once the window resets.

Workspaces & teams

Gumhop has shared team workspaces, but the v1 API is scoped to the account that owns the key — it reads and writes only the folders, lists, and tasks you created. That spans your personal space and anything you authored in a team space, but it never includes content a teammate created in a shared workspace, even though the app shows that content to you.

  • GET /lists and GET /tasks return only your own rows. Filtering by a teammate's list_id yields an empty result; a teammate's task id returns 404.
  • POST /tasks targets a list you own — adding a task to a teammate's list returns 404. Omitting list_id drops the task into your first owned list.
  • POST /lists creates the list in your Personal workspace, unless you nest it under a folder_idyou own in another workspace — then the list inherits that folder's workspace.
  • Every folder, list, and task carries a read-only workspace_id. There are no v1 endpoints for workspaces, members, invites, assignees, comments, mentions, or notifications yet.

Endpoints

GET/me

Returns the account the key belongs to.

{ "user_id": "uuid", "email": "you@example.com", "display_name": "Sara", "timezone": "America/New_York" }
GET/lists

Every folder and list you own, each ordered by sidebar position. See Workspaces & teams for how this maps to shared spaces.

{
  "folders": [{ "id": "uuid", "name": "Personal", "workspace_id": "uuid", "icon": null, "color": null, "sort_key": "a0" }],
  "lists": [{ "id": "uuid", "name": "Work", "workspace_id": "uuid", "folder_id": null, "icon": null, "color": null, "sort_key": "a0" }]
}
POST/lists

Create a list. Optional folder_id nests it under a folder you own. Returns 201 with the created { "list": { … } }.

curl -X POST https://www.gumhop.com/api/v1/lists \
  -H "Authorization: Bearer gh_live_…" \
  -H "Content-Type: application/json" \
  -d '{"name":"Work","folder_id":null}'
GET/tasks?list_id=&status=todo

List tasks, ordered by sidebar position. Optional query params: list_id, status (todo/done). Returns { "tasks": [ … ] }; each task has this shape:

{
  "id": "uuid", "workspace_id": "uuid", "list_id": "uuid", "title": "Renew passport",
  "notes": null, "original_text": "renew passport",
  "status": "todo", "priority": 3, "pinned": false,
  "due_date": "2026-07-01", "due_time": null,
  "chips": [], "tags": [], "ai_status": "none", "source": "api",
  "completed_at": null, "created_at": "…", "updated_at": "…"
}

tags and original_text are returned on reads but cannot be set through the API.

GET/tasks/:id

Fetch a single task by id. Returns { "task": { … } } with the same fields shown under GET /tasks.

POST/tasks

Create a task two ways. Mode A — natural language with AI parsing:

curl -X POST https://www.gumhop.com/api/v1/tasks \
  -H "Authorization: Bearer gh_live_…" \
  -H "Content-Type: application/json" \
  -d '{"text":"email the deck to sara@acme.com friday","parse":true}'

When you send text, AI parsing is on by default — the parse field is optional (pass {"parse":false} to store the raw text verbatim). The parser extracts a clean title, due date/time, priority, notes, and contact/location/link/meeting chips. Detected subtasks are not created via the API.

Parsing draws on a separate budget of ~40 parses/minute per account, shared with in-app capture. If you exceed it — or AI parsing is unavailable — the task is still created with your raw text as the title and no extracted fields, and the response is still 201.

Mode B — explicit structured fields:

-d '{"title":"Renew passport","due_date":"2026-07-01","priority":3}'

Accepted fields: title, notes, list_id, due_date, due_time, priority (0-3, clamped), pinned, chips. Omit list_idto drop the task into your first list — it's an error to create a task when the account has no lists. New API tasks appear at the top of the list.

PATCH/tasks/:id

Update fields. Setting {"status":"done"} completes a task; {"status":"todo"} reopens it and clears completed_at.

curl -X PATCH https://www.gumhop.com/api/v1/tasks/<id> \
  -H "Authorization: Bearer gh_live_…" \
  -H "Content-Type: application/json" \
  -d '{"status":"done"}'
DELETE/tasks/:id

Permanently delete a task. Returns { "deleted": true }. Completed tasks may also be auto-deleted if you enable cleanup in Settings (off by default).

POST/rewards/complete

Claim the gacha reward for a completed task — the same once-ever card draw the app fires when you check a task off (the Chrome extension uses this endpoint). Completing a task via PATCH /tasks/:id does not draw by itself; call this afterwards if you want the pull. Calling it on a still-open task marks the task done first.

curl -X POST https://www.gumhop.com/api/v1/rewards/complete \
  -H "Authorization: Bearer gh_live_…" \
  -H "Content-Type: application/json" \
  -d '{"task_id":"<id>"}'

A successful first claim returns the drawn card:

{
  "ok": true,
  "reward": {
    "kind": "image", "imageUrl": "https://…", "message": "Nice work!",
    "packName": "Treat yourself", "streakToday": 3, "rarity": "shiny"
  },
  "popup": true
}

rarity is one of common / shiny / holographic / legendary. For media you uploaded, imageUrl is a short-lived signed URL (~10 minutes) — display it promptly rather than storing it; externally-hosted items pass their original URL through. popupmirrors the account's popup-celebration preference. Email/push delivery still follows your Settings, same as in-app completions.

A task rewards exactly once, ever: repeat calls return { "ok": true, "reward": null, "alreadyRewarded": true }. If no active pack has items yet, the claim is released and the response is { "ok": true, "reward": null, "empty": true } — the task can still reward later once a pack has cards.

Webhooks

Set a webhook URL in Settings → API. Gumhop POSTs task.created and task.completed events:

Webhooks are user-scoped, not workspace-scoped: each event is delivered only to the webhook URL of the task's owner — the account that created it. You receive events for tasks you created (including when a teammate completes one of yours), but never for tasks a teammate owns, even in a shared workspace.

{
  "event": "task.completed",
  "task": {
    "id": "uuid", "title": "...", "status": "done",
    "priority": 2, "pinned": false,
    "due_date": "2026-07-01", "due_time": "14:30:00",
    "notes": null, "source": "api", "ai_status": "none",
    "list_id": "uuid", "chips": [], "completed_at": "...", "created_at": "..."
  }
}

The payload is a subset of the full task object — it omits tags, original_text, and updated_at, which are present on GET responses. source is api for tasks created via this API and app for tasks created in the app.

Verifying signatures

If you set a signing secret, each delivery includes an X-Gumhop-Signature-256 header: sha256=<hmac> of the raw body.

// Node.js
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
# Python
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(header, expected)

Using Gumhop from an AI agent

  1. Create an API key and store it as a secret in your agent runtime.
  2. To capture a task, POST /tasks with {"text":"…","parse":true} and let Gumhop extract a clean title, due date/time, priority, notes, and contact/location/link/meeting chips. Subtasks the parser detects are not created via the API.
  3. To check progress, GET /tasks?status=todo.
  4. To complete a task, PATCH /tasks/:id with {"status":"done"} — which also fires your task.completed webhook.
  5. To give the user their card pull for that completion, POST /rewards/complete with {"task_id":"…"}— idempotent, so it's always safe to call.