Authentication
Create an API key in Settings → API. Pass it as a Bearer token on every request. Keys are shown once, so 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 /listsandGET /tasksreturn only your own rows. Filtering by a teammate'slist_idyields an empty result; a teammate's task id returns 404.POST /taskstargets a list you own; adding a task to a teammate's list returns 404. Omittinglist_iddrops the task into your first owned list.POST /listscreates the list in your Personal workspace, unless you nest it under afolder_idyou own in another workspace, in which case the list inherits that folder's workspace.- Dependencies (“blocked by” links) follow the same rule: both tasks must be ones you created, in the same workspace, and you must still be a member who can edit it. Links to a teammate's task are left out of
blocked_byandGET /dependencies, thoughopen_blocker_countstill counts every open blocker. - 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
/meReturns the account the key belongs to.
{ "user_id": "uuid", "email": "you@example.com", "display_name": "Sara", "timezone": "America/New_York" }/listsEvery 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" }]
}/listsCreate 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}'/tasks?list_id=&status=todoList 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.
Every task also carries a read-only open_blocker_count: how many of the tasks it is blocked by are still open. A todo task with a count above 0 shows as Blocked in the app.
/tasks/:idFetch a single task by id. Returns { "task": { … } } with the same fields shown under GET /tasks.
The single-task read also includes blocked_by: the ids of the tasks this one is blocked by (open or done) that you created. See dependencies.
/tasksCreate 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, so 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. Steps it detects are created as subtasks, exactly as in-app capture does.
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, start_date, repeat, priority (0-3, clamped), pinned, chips. Omit list_id to 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.
/tasks/:idUpdate fields. Setting {"status":"done"} completes a task; {"status":"todo"} reopens it and clears completed_at.
Completing a blocked task through the API does not ask for confirmation (the app does). Check open_blocker_count first if that matters to your integration.
curl -X PATCH https://www.gumhop.com/api/v1/tasks/<id> \
-H "Authorization: Bearer gh_live_…" \
-H "Content-Type: application/json" \
-d '{"status":"done"}'/tasks/:idPermanently delete a task. Returns { "deleted": true }. Completed tasks may also be auto-deleted if you enable cleanup in Settings (off by default).
/dependencies?task_id=A task's “blocked by” links in both directions: blocked_by (tasks that should be done first) and blocking (tasks waiting on this one). Only tasks you created are listed.
{
"blocked_by": [{ "id": "uuid", "title": "Get quotes", "status": "todo", "list_id": "uuid", "due_date": null, "workspace_id": "uuid" }],
"blocking": []
}/dependenciesMark blocked_id as blocked by blocker_id. Both tasks must be yours and in the same workspace. Returns 201 with { "dependency": { … } }, or 200 with the existing link if it is already there.
curl -X POST https://www.gumhop.com/api/v1/dependencies \
-H "Authorization: Bearer gh_live_…" \
-H "Content-Type: application/json" \
-d '{"blocker_id":"<id>","blocked_id":"<id>"}'409 if the link would create a loop (the blocker already waits on the other task, directly or through others). 422 for a task blocking itself, tasks in different workspaces, or more than 50 blockers on one task. 404 if either task isn't yours.
/dependencies?blocker_id=&blocked_id=Remove a link. Returns { "deleted": true }, or 404 if there is no such link.
/rewards/completeClaim 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), so display it promptly rather than storing it; externally-hosted items pass their original URL through. popup mirrors 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.
Recurring series
A series is a blueprint plus a schedule. The next copy doesn't exist until its time arrives: between cycles there is no task, only the series. Each copy is an ordinary task with series_id set, so it fires task.created, rewards once, and behaves like any other task. Series are workspace-scoped: anyone who can edit the workspace can read and manage them.
The repeat object
{
"mode": "fixed", // "fixed" | "after_completion" (default "fixed")
"unit": "week", // "day" | "week" | "month" | "year" (required)
"every": 2, // default 1
"weekdays": ["mon","thu"], // week only
"month_day": 31, // 1..31 | -1 (last day); month/year
"nth": -1, "nth_weekday": "fri", // month/year, instead of month_day
"month": 3, // year only
"starts_on": "2026-09-22", // fixed only; default: the task's due date, else today
"appears": "08:00", // "HH:MM" | "exact" (default "08:00")
"due": "appear_day", // "none" | "appear_day" | "offset"
"due_offset_days": 0,
"if_open": "skip", // "skip" | "shift" | "stack" (fixed only)
"ends_on": null, // at most one of ends_on / ends_after
"ends_after": null,
"push": true // push when a copy appears
}An invalid rule returns 400 with {"error":"invalid repeat: …"}. Month-end days clamp (Jan 31 → Feb 28); after_completion rules are interval-only, so they take no weekdays, month day, nth or month.
/seriesEvery series in the workspaces you can write. Optional status (active/paused/ended) and workspace_id. Returns { "series": [ … ] }; each row carries its schedule, its stats (spawned_count, completed_count, skipped_count, last_completed_at), next_on / next_at, and an open array of its current copies.
/seriesCreate a schedule from scratch:
curl -X POST https://www.gumhop.com/api/v1/series \
-H "Authorization: Bearer gh_live_…" \
-H "Content-Type: application/json" \
-d '{"title":"Team sync","list_id":"<id>","due_time":"09:00",
"repeat":{"unit":"week","every":2,"weekdays":["tue"],"appears":"09:00"}}'Accepted fields: title, list_id, notes, priority, tags, chips, checklist (an array of strings, copied unchecked onto every copy), due_time and repeat. Or turn an existing task into the current copy: {"from_task_id":"<id>","repeat":{…}}.
/series/:idOne series, with its open copies. Returns { "series": { … } }.
/series/:idUpdate the blueprint (title, notes, priority, tags, chips, checklist, due_time, list_id), merge a partial repeat, or set {"status":"paused"} / {"status":"active"} to pause and resume. Blueprint edits also reach the open copies.
/series/:id/skipSkip this time: the open copy is dismissed with no reward and no completion, and the next occurrence is scheduled. While waiting between copies it skips the next occurrence instead. Pass {"task_id":"<id>"} to name the copy.
/series/:id/spawnCreate the next copy now and move the schedule on past it. Returns { "series": {…}, "task_id": "<id>" }. Limited to 20 per hour; an after-completion series refuses (409) while a copy is still open.
/series/:idDelete the blueprint; existing copies are kept as plain tasks. Add ?stop_only=1 to end the series but keep the row (and its stats).
Repeats on tasks
POST /tasks and PATCH /tasks/:id both accept a repeat object: on a task without one it creates the series (that task becomes the current copy), and on a task that already has one it updates the schedule. {"repeat":null} turns repeating off: the series ends and the task stays as a plain one, with series_id cleared. Responses include series alongside task.
start_date (on tasks) hides a task from Today and the Board until that day; it waits in its list's "Later" section. Every new copy of a series fires a task.created webhook.
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, plus shortcut, email, slack, discord and automation for the other doors. You may send {"source":"shortcut"} on a create; any other value is ignored (an API key can't label a task as an automation or an email).
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
- Create an API key and store it as a secret in your agent runtime.
- Give the agent this reference as Markdown: www.gumhop.com/docs/api.md, also served at
GET /docsunder the base URL. One fetch, no key needed. - To capture a task, POST
/taskswith{"text":"…","parse":true}and let Gumhop extract a clean title, due date/time, priority, notes, and contact/location/link/meeting chips. Steps the parser detects become subtasks. - To make it repeat, say "every month" in
textwith{"parse":true}, or send an explicitrepeatobject. - To check progress, GET
/tasks?status=todo. - To complete a task, PATCH
/tasks/:idwith{"status":"done"}, which also fires yourtask.completedwebhook. - To give the user their card pull for that completion, POST
/rewards/completewith{"task_id":"…"}. It's idempotent, so it's always safe to call.