# Floatee — Integration guide for LLM agents (Claude Code & co.) Floatee is a centralized store of todos, ideas and notes, agent-first. It replaces the scattered `TODOs.md` files in each repo with a web app (PWA) plus a REST API your coding agent can drive: fetch the next todo, comment, mark it done with a resolution note, search the full history. This file is the complete, self-contained API reference — an agent that reads only this file has everything it needs (you never need the app's source). The human-facing app lives at https://floatee.me/ ; this API is at https://floatee.me/api/v1. ## Getting a token Tokens are **account-scoped**: one user can mint several (one per machine, labelled "laptop", "work PC", …). The human user creates one in the Floatee UI under **Settings → Your agent** — you do NOT create tokens yourself. A token looks like `flt_…` and is shown once. The **same token works in every repo**. Which project a call targets is passed per request (`?project=`), not baked into the token. The token lives in a `.floatee` file at the repo root (gitignored): token=flt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx api=https://floatee.me/api/v1 project=my-repo `project` pins the project slug for this repo. A slug that doesn't exist yet is **auto-created** on first use — including by a plain GET, so a typo costs a real (empty) project that counts against your plan. Two levers: - **`&create=0`** on any project-scoped call → `404 project_not_found` instead of creating. That is how you CHECK a slug before using it. - **`&workspace=`** → where a new project lands (default: your personal workspace). Must be a workspace you belong to; see `GET /workspaces`. When a call does create the project, the response carries `X-Floatee-Project-Created: ; workspace=` — an effect that big is never silent. If `project` is omitted, use the repo folder name as the slug. Treat the token as a secret: never commit it, never print it, never log it. ## Conventions - Base URL: `https://floatee.me/api/v1` - Auth: header `Authorization: Bearer flt_…` on **every** request. - **Every** call takes **`?project=`** — *including item-by-id* (`/items/:id`, `/items/:id/comments`). An item's id is the **per-workspace `#N`** (assigned on create, unique within its workspace); the `?project` you pass resolves that workspace. Note two projects in the same workspace share one `#N` sequence, so the numbers a single project shows can have gaps. Omitting `?project` → `400 project_required`. - Every response carries an **`X-Floatee-Kit-Version`** header (the current agent-kit version). The `floatee` helper CLIs read it to self-update; if you call the API directly you can ignore it. - GET responses are **markdown by default**. Add **`?format=json`** for JSON. - Writes (POST / PATCH / DELETE) take and return **JSON** (`Content-Type: application/json`). - `body_md`, comments and `resolution_note` are **markdown** in full **UTF-8**: send real accents/apostrophes and markdown (`code`, **bold**, lists; a blank line for a real line break — a lone `\n` is only a soft wrap). Build the JSON with an encoder and send it from a file (`curl --data @file`); don't hand-escape an inline `-d '…'` (your shell will mangle UTF-8/quotes). - **Images.** `body_md` may contain markdown images `![alt](url)` where the URL is a root-relative attachment path like `/api/workspaces//attachments/`. To view one, GET that path against the API host with your **`Authorization: Bearer flt_…`** (same token) — it returns the raw image bytes. Resolve a root-relative URL against the API origin (e.g. `https://floatee.me` + the path). Preserve these links verbatim when you edit a `body_md`. Uploading new images over the API isn't supported yet — humans add them from the app. - Field names accept both `snake_case` and `camelCase` (e.g. `body_md` or `bodyMd`, `resolution_note` or `resolutionNote`). This doc uses snake_case. - Errors: JSON `{ "error": "...", "code": "..." }` with an HTTP status: - `400 project_required` — missing `?project`. - `400 body_required` / `invalid_status` / `invalid_type` / `no_changes` — bad payload. - `401` — invalid / revoked token. - `403 plan_limit` / `plan_required` — a plan gate (see "Plan limits"). - `404 not_found` — item not in this project / deleted. - `429` — rate limit (back off and retry). ## Item model Fields you read (JSON) / set (writes): - **id** — the per-workspace `#N` (integer, ≥ 1). Read-only; assigned on create. - **type** — `todo` | `idea` | `note`. Default `todo`. Only `todo` is returned by `/next`. Promote an idea/note to a todo by PATCHing `type` — it keeps its id, tags and history. - **status** (todos) — `pending` | `in_progress` | `blocked` | `done` | `wontfix`. New items start `pending`. `done`/`wontfix` should carry a `resolution_note` (and ideally a `conventional_type`). - **body_md** — markdown; **the whole item**. Its **first line is the title** (a leading `#`/`>` marker is stripped for display). GFM task lines `- [ ]` / `- [x]` become an interactive checklist with a `done/total` count. You do NOT send a separate `title` — it is derived from the first line. (An explicit `title` is accepted and wins, but normally just write `body_md`.) - **tags** — array of plain strings, e.g. `["bug","ui"]` (no `#`/`+` prefix; any leading `#`/`+` is stripped). See "Editing tags" — a write **replaces the whole array**. - **snooze_until** — an **absolute Unix timestamp in seconds**, or `null`. A snoozed item is "sleeping": excluded from `/next` and from the default list until that time passes, then it auto-wakes (lazy, no cron). NOT a duration string — compute the epoch yourself (see "Snoozing"). - **resolution_note** — short "what was done" string, set when resolving. - **conventional_type** — `feat` | `fix` | `chore` | `perf` | `refactor` | `docs` | `style` | `test` | `build` | `ci` | … , optionally suffixed `!` for a breaking change (e.g. `feat!`). Feeds the changelog. - **position** (create only) — `top` | `bottom` (default `bottom`). Where the new item lands in the manual sort order. - **assignee** — who owns the item. One assignee at most: a workspace member (read `assignee_id` / `assigneeName`, `assigneeType: "user"`) OR an agent token (read `assignee_token_id` / `assigneeName`, `assigneeType: "token"`). On a write send `assignee_id` to assign a member, `assignee_token_id` to assign an agent (setting one clears the other), or `null` to unassign. Friendlier: send **`assignee_handle`** — a `handle` from `GET /project`'s `assignees` list, or **`me`** for yourself (your token). New items default to their creator — an item you create via the API is assigned to **your token** unless you set an assignee. References inside text: writing `#N` anywhere in `body_md` or a comment links to ticket `#N` of the same project (rendered as a link in the app). It's plain text — no special call needed. ## Endpoints ### GET /projects Every project you can reach, grouped by workspace, with its open-item count. Takes **no** `?project` and creates nothing — this is the call to make when you don't know the slug yet, instead of guessing one into existence. `&include_archived=1` to also list archived ones. `&format=json` for JSON. ### GET /workspaces The workspaces you belong to (`slug`, `name`, `isPersonal`, `isOwner`, project count). These slugs are what `&workspace=` accepts when a new project has to land somewhere other than your personal workspace. Creates nothing. ### GET /next?project= First `pending` `todo`, not snoozed, by manual sort order. Returns the full item (body, tags, activity). Optional `&assignee=` — match a member by id or display name, an agent by token id or label, or `&assignee=me` for the todos assigned to your own token (the agent's personal queue). `204 No Content` when nothing is pending. Add **`&claim=1`** to atomically **mark the returned item `in_progress`** in the same call (fetch + lock in one round-trip — no separate PATCH, and race-safe across agents). Prefer this when processing: it replaces step 1+2 of the flow below. curl -H "Authorization: Bearer $TOKEN" \ "https://floatee.me/api/v1/next?project=my-repo&claim=1&format=json" ### GET /items?project= List items. Query params: - `status` — exact status filter (`pending`/`in_progress`/`blocked`/`done`/`wontfix`). - `type` — `todo`/`idea`/`note`. - `tag` — items carrying this tag. - `q` — full-text filter (see /search for the index covered). - `include_done` — `1` to drop the default status filter (returns ALL statuses, incl. done/wontfix). Ignored if `status` is set. - `sleeping` — `exclude` (default) / `include` / `only`. Snoozed items are hidden unless you ask; **`only` answers "what did I postpone?"**. This applies whatever `status` or `include_done` say. A listed sleeping item is marked `💤 until ` in the markdown, since sleep isn't a status and nothing else would show it. - `limit` (default 50, max 200), `offset` (default 0). - Default (no `status`, no `include_done`): open work only (`pending`/`in_progress`/`blocked`), not snoozed. ### GET /items/:id?project= Full detail (item + activity feed). `:id` is the per-workspace `#N`. Markdown by default; `&format=json` for JSON. ### POST /items?project= Create an item. JSON body — `body_md` is required (its first line is the title): `body_md`, `type`, `tags`, `position`, `snooze_until`, `conventional_type`, `assignee_id` / `assignee_token_id` / `assignee_handle` (a handle from `GET /project`, or `me`). Returns the created item (JSON, `201`). New items are always `pending`, and assigned to your token unless you set an assignee. curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"body_md":"Fix slider overflow\n\nThe range input bleeds past the card.","type":"todo","tags":["bug","ui"],"position":"top"}' \ "https://floatee.me/api/v1/items?project=my-repo" ### PATCH /items/:id?project= Partial update. Any of: `status`, `resolution_note`, `conventional_type`, `body_md`, `title`, `tags`, `type`, `snooze_until`, `assignee_id`, `assignee_token_id`, `assignee_handle` (or `me`). Only the keys you send change. Returns the fresh item (JSON). Updating `body_md` re-derives the title from its first line **unless the same call also sends `title`** — so a body edit renames the item by default. A `title` sent on its own leaves the body untouched, and since the item markdown renders the body's first line as its heading, that title only shows up in lists and in the app. (To reorder the list, prefer `POST /items/reorder` below — it computes the sort order for you.) `body_md` is a **full replace** — there is no append. Read the current body from `GET /items/:id?format=json` (the `bodyMd` field, **not** the rendered `markdown`, which also contains the metadata and the comment thread) and send it back whole. With the CLI: `floatee get 142 --raw`, or `floatee edit 142 --append -m "…"`. curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"status":"done","resolution_note":"Added px-4 to the container","conventional_type":"fix"}' \ "https://floatee.me/api/v1/items/142?project=my-repo" ### POST /items/reorder?project= Reorder the backlog in one call — reprioritise before you start work. JSON: `{ "order": [40, 12, 7], "position": "top" }`. `order` is the list of item ids (the per-workspace `#N`) in the exact top-to-bottom order you want; the server assigns the sort orders (you never compute them). The listed items become one contiguous block at the **top** of the project list, in that order; items you omit keep their relative order below. Set `"position": "bottom"` to anchor the block at the bottom instead. Unknown ids (or ids from another project) are skipped, not fatal. Returns `{ "ok": true, "moved": [...], "skipped": [...] }` (`moved` = the ids in their applied order). curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"order":[40,12,7]}' \ "https://floatee.me/api/v1/items/reorder?project=my-repo" ### DELETE /items/:id?project= Soft delete — recoverable, but excluded from lists and search. ### POST /items/:id/comments?project= Append a markdown comment to the item's activity feed. JSON: `body_md`. Put long analyses / post-mortems here rather than in the item body. curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"body_md":"Reproduced on iOS Safari only; the container lacks horizontal padding."}' \ "https://floatee.me/api/v1/items/142/comments?project=my-repo" Returns `{"ok":true,"comment":"c3","commentSeq":3}` — **`c3` is the comment's public id**, per item, and what the two calls below take. ### PATCH /items/:id/comments/:cN?project= Fix a comment you posted. JSON: `body_md` (replaces it). `:cN` is the `[c3]` id the item markdown prints (`3` works too). The item's activity marks it `· edited` from then on. Returns the fresh item, so you can relay the updated thread without a GET. curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"body_md":"Correction: it also happens on Chrome Android."}' \ "https://floatee.me/api/v1/items/142/comments/c3?project=my-repo" ### DELETE /items/:id/comments/:cN?project= Remove a comment for good (a real delete, not a soft one). Returns the fresh item. **Who may edit or delete:** your token can touch **what that token posted**; if the token's user owns the workspace, it can also clean up **any agent's** comment. A human's comment is never editable from the API (`403 forbidden`). After rotating a token, the new one can't fix the old one's comments unless it owns the workspace. **Ids are stable:** `cN` is stored, not recomputed, so deleting `c1` leaves `c2` as `c2`, and the next comment gets `c3` — an id you noted stays pointing at the same message. `404 comment_not_found` if it's gone. ### GET /search?q=&project= Full-text over the project — titles, bodies, resolution notes, tags AND comments. Includes done/wontfix, so you can check whether an approach was already tried/abandoned (a wontfix often carries *why*). Most-recently-resolved first. ### GET /changelog?project= Done items carrying a `conventional_type`, grouped by resolution date, in `changelog.json` shape (`{ "entries": [...], "ids": [...] }`). Params: `since` (date or epoch), `unexported=1`. Pair with the mark endpoint below. *Pro plan.* ### POST /changelog/mark-exported?project= Mark items as changelog-exported so a later `?unexported=1` won't re-emit them. JSON: `{ "ids": [12, 15, 18] }`. *Pro plan.* ### GET /project?project= Project metadata: name, slug, color, icon, archived flag, counts by status (plus idea/note totals), and **`assignees`** — the people you can assign a todo to: members (`type: "user"`) and agents (`type: "token"`), each with a `handle` and a display `name`. Pass a `handle` as `assignee_handle` on a create/patch (or `me` for yourself). Markdown by default; `&format=json` for JSON. ## GET output shape (markdown, the default) `GET /items/:id` returns, e.g.: # [#142] Fix the slider overflow on mobile - status: in_progress · type: todo · tags: bug, ui - created: 2026-06-02 by françois - sleeping until: 2026-07-01 (only if snoozed) - resolution: … (only if resolved) The label runs off-screen on mobile when the value is long. - [ ] check iOS Safari - [x] check RTL ## Activity - 2026-06-03 · status_change pending → in_progress - 2026-06-03 · comment (agent "Claude Code — laptop"): Reproduced. The container has no horizontal padding… List/search GETs return one line per item: `- [#142] — status: … · type: … · tags: … · 3/7`. Use `?format=json` on any GET if you'd rather parse structured data. **One call for both**: single-item JSON responses (`GET /next?…&format=json`, `GET /items/:id?…&format=json`, and the JSON returned by a create `POST` and a `PATCH`) also include a **`markdown`** field — the exact rendering shown above. So one call gives you the relayable markdown AND the structured fields (id, status, tags…). A `PATCH` response carries the post-change `markdown` too, so a close can be summarised from it without a follow-up GET. (List responses omit `markdown`.) ## Recipes (exactly how to do common things) ### Editing tags — a write REPLACES the whole array There is no add/remove-one endpoint. To **add** a tag, read the current tags, append, and PATCH the full new array: # 1) read current tags curl -H "Authorization: Bearer $TOKEN" \ "https://floatee.me/api/v1/items/142?project=my-repo&format=json" # → "tags":["bug"] # 2) PATCH the complete desired set curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"tags":["bug","ui"]}' \ "https://floatee.me/api/v1/items/142?project=my-repo" To **remove** a tag: PATCH the array without it. To **clear all**: `{"tags":[]}`. ### Snoozing — snooze_until is an absolute Unix epoch (seconds) Compute the timestamp yourself; don't send a duration. E.g. snooze ~2 weeks: UNTIL=$(date -d '+14 days' +%s) # GNU date; macOS: date -v+14d +%s curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d "{\"snooze_until\": $UNTIL}" \ "https://floatee.me/api/v1/items/142?project=my-repo" Wake it now (un-snooze): `{"snooze_until": null}`. With the `floatee` CLI, skip the epoch maths entirely: `floatee snooze 142 2w` (also `12h`, `3d`, `1m`, `1y`, or `2026-09-01[T09:00]` in your local time) and `floatee wake 142`. ### Promote an idea/note to a todo `PATCH {"type":"todo"}` (keeps id, tags, history). ### Block an item you can't finish autonomously `PATCH {"status":"blocked"}`, then POST a comment stating *exactly* what info or decision you need so the human can unblock by replying. ### Reference another ticket Write `#N` in the `body_md` or a comment — it links to ticket `#N` of the same project. ### Create a todo at the top of the list `POST {"body_md":"…","position":"top"}`. ## Plan limits (why a 403 happens) - **Free**: caps the number of *active* items per account (history — done/ wontfix/deleted — is never counted). Over the cap → `403 plan_limit`. - **Snooze** is a Pro+ feature: setting `snooze_until` on Free → `403 plan_required`. Clearing it (`null`) is always allowed. - **Changelog** endpoints are Pro+ → `403 plan_required`. ## Typical agent flow ("process the next todo") 1. Read `.floatee` → token + api + project. 2. `GET /next?project=<project>&claim=1&format=json` → fetches the next todo AND locks it `in_progress` in one call (`204` = nothing to do; stop). The response's `markdown` field is what you show; its structured fields drive the rest. (Without `claim=1` you'd need a separate `PATCH {"status":"in_progress"}` — the lock that stops another agent taking the same item.) 3. `GET /search?q=<keywords>&project=<project>` if it looks like something tried before (a wontfix carries why it was abandoned). 4. Do the fix. Post any long analysis as a comment. 5. `PATCH /items/{id}?project=<project>` `{"status":"done","resolution_note":"…","conventional_type":"fix","tags":[…]}` (or `wontfix` + note if dropped; `blocked` + comment if you're stuck). The PATCH response includes the post-change `markdown`, so summarise from it — no extra GET. **Auto-tag on close**: add 1–3 concise tags for the area/nature of the work, MERGED with the item's existing tags — `tags` is a full-array replace, so send existing ∪ new (never drop a tag already there). See "Editing tags". One todo per invocation. Always re-fetch the item; don't trust prior context. If you find an unrelated problem mid-fix, file it (`POST /items?project=…` with tag `found-by-agent`) instead of interrupting the user. ## Any agent, one block These same rules are summarised in the AGENTS.md block shown on the project's "Set up your agent" tab. Paste it into your repo's `AGENTS.md` and any tool that reads it — Claude Code, Codex, Copilot, Cursor, Gemini CLI, Aider, Zed, Windsurf — knows how to process the project's todos. Claude Code users also get a `/todo` slash command. The downloadable kits come in two flavours. The **helper builds** (the `floatee` CLI for Node, Python or shell) **self-update**: every response advertises the kit version, so when a newer kit ships the next command transparently refreshes the CLI and the `/todo` commands (your `## Custom instructions` blocks are kept). Run `floatee selfupdate` to force it, or set `auto_update=off` in `.floatee` to disable the automatic refresh (you still get a notice + the manual command). The **curl build** has no executable, so it only tells you when an update is available — re-download it, or switch to a helper build.