# Gumhop API reference

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

- Base URL: `https://www.gumhop.com/api/v1`. Endpoint paths below are relative to it.
- Auth: `Authorization: Bearer gh_live_…` on every request. Create a key at https://www.gumhop.com/settings/api.
- This document: https://www.gumhop.com/docs/api.md (also `GET https://www.gumhop.com/api/v1/docs`). Web version: https://www.gumhop.com/docs/api.

## Authentication

Create an API key in [Settings → API](https://www.gumhop.com/settings/api). Pass it as a Bearer token on every request. Keys are shown once, so store them safely.

```bash
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.

```json
{ "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.

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

```bash
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:

```json
{
  "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:

```bash
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:

```bash
-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.

```bash
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.

```json
{
  "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.

```bash
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.

```bash
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:

```json
{
  "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

```jsonc
{
  "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:

```bash
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](https://www.gumhop.com/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.

```json
{
  "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.

```js
// 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
# 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](https://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](https://www.gumhop.com/docs/integrations).

Questions? Manage keys and webhooks in [Settings → API](https://www.gumhop.com/settings/api).
