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, 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 /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_id you 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_by and GET /dependencies, though open_blocker_count still 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

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.

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.

GET/tasks/:id

Fetch 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.

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, 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.

PATCH/tasks/:id

Update 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"}'
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).

GET/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": []
}
POST/dependencies

Mark 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.

DELETE/dependencies?blocker_id=&blocked_id=

Remove a link. Returns { "deleted": true }, or 404 if there is no such link.

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), 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.

GET/series

Every 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.

POST/series

Create 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":{…}}.

GET/series/:id

One series, with its open copies. Returns { "series": { … } }.

PATCH/series/:id

Update 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.

POST/series/:id/skip

Skip 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.

POST/series/:id/spawn

Create 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.

DELETE/series/:id

Delete 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

  1. Create an API key and store it as a secret in your agent runtime.
  2. Give the agent this reference as Markdown: www.gumhop.com/docs/api.md, also served at GET /docs under the base URL. One fetch, no key needed.
  3. 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. Steps the parser detects become subtasks.
  4. To make it repeat, say "every month" in text with {"parse":true}, or send an explicit repeat object.
  5. To check progress, GET /tasks?status=todo.
  6. To complete a task, PATCH /tasks/:id with {"status":"done"}, which also fires your task.completed webhook.
  7. To give the user their card pull for that completion, POST /rewards/complete with {"task_id":"…"}. It's idempotent, so it's always safe to call.

No-code recipes for Zapier, Make and n8n, the calendar feed, email-to-task and the iOS Shortcut are in Integrations.

Questions? Manage keys and webhooks in Settings → API.