API v1
coLab API
Project, task, note, decision and chat work, as a person.
/api/v1 lets a script, a terminal or an agent do the work the app does — list,
create, change, advance and delete — as a person, reaching exactly what that person can reach.
Authentication
Every endpoint except the two device-flow ones takes a bearer token:
curl https://colab.neurasense.io/api/v1/tasks?assignee=me&status=todo \
-H "Authorization: Bearer colab_pat_…"
Get one of two ways. By hand, from Account → API tokens: name it, choose read or write, choose
an expiry, and optionally pin it to one workspace. It is shown once and only its SHA-256 is stored.
From a terminal, with the device flow below — the better one, because the secret is never
displayed, retyped, or left in shell history.
Creating a token either way asks you to prove who you are again, with a passkey where one is
enrolled and your password where none is. A thirty-day-old session cookie is the right bar for
moving a task and the wrong one for minting a credential that works from anywhere until revoked.
The device flow
Modelled on the OAuth device authorization grant (RFC 8628),
so a client written against any other device flow already knows what to do with it.
# 1. ask to be let in, and start polling
curl -X POST https://colab.neurasense.io/api/v1/device/code \
-H 'Content-Type: application/json' \
-d '{"client_name":"my integration","scope":"write"}'
# → { "user_code": "WDJB-MJHT", "verification_uri_complete": "https://colab.neurasense.io/link?code=WDJB-MJHT", … }
# 2. the person opens that URL, sees what is asking, and approves it with a passkey
# 3. the next poll is answered with a token
curl -X POST https://colab.neurasense.io/api/v1/device/token \
-H 'Content-Type: application/json' -d '{"device_code":"…"}'
Poll no faster than the interval you were given (5s). Pending polls answer
400 authorization_pending; polling too fast answers 429 slow_down. A code expires after
10 minutes, and buys exactly one token.
What a token cannot do
- Reach further than its owner. Every request goes through the same permission check the
browser does.
write scope on a project you cannot open is still a 404.
- Leave its workspace, if one was pinned — answered with the same 404 as a project that does
not exist, because the pin is a confinement rather than a hint about what lives elsewhere.
- Widen anything. Scope only narrows: a
read token is refused a write with
403 insufficient_scope, and write grants nothing its owner did not already have.
Conventions
- Lists page with a cursor, not an offset — rows get created while a client is walking a list,
and an offset would quietly skip one each time. Follow
next_cursor until it is null.
- On a PATCH, omitted is not null. A field left out is left alone; a field sent as
null is
cleared.
- Dates are calendar days (
YYYY-MM-DD); timestamps are ISO 8601 instants.
- Failures are always
{"error": {"code": …, "message": …}}. Switch on code. A token that
never existed, one that was revoked and one that expired all answer 401 invalid_token — which
of the three is not something the holder of a stolen token should learn from us.
Device flow
Pairing a client that has no browser of its own.
POST/device/codeno token
Ask to be let in
Unauthenticated by necessity — the caller has no credential yet. What comes back is a pending request that stays worthless until somebody signed in approves it.
Body
client_name | string | Shown to the person approving. Say what and where you are. |
scope | "read" | "write" | Defaults to read. |
POST/device/tokenno token
Poll for the token
Answered with a token once somebody has approved the pairing, and until then with a reason to keep waiting. A pending poll is a 400 with a body rather than a 200, which is what RFC 8628 specifies and what stops a client treating 'not yet' as success. A device code buys exactly one token.
Body
device_coderequired | string | |
Identity
Who a token is.
GET/me
Who this token is
The endpoint to call first. It confirms a pairing worked, and hands back a workspace id so a client can create things without an operator digging one out of a URL.
Search
One query across everything a token can reach.
GET/search
Search everything
One query across projects, tasks, notes, milestones, decisions, comments, chat messages, attachments, links, previews and review pins — everything the token's owner can reach. This is the same search the app's own ⌘K palette runs.
A leading @Name narrows to one person's work, and @me is whoever the token belongs to. Wrapping the query in double quotes means exactly this, and suppresses spelling correction. project_id confines the search to one accessible project; kinds=note makes it a note-only content search.
Not paged: this is a bounded, ranked set rather than a walk through a table, and a cursor over relevance would be a cursor over an order that moves.
Parameters
qrequired | string | What to look for. Supports a leading @Name or @me, and "exact phrases". |
workspace_id | string | Needed only when you belong to more than one and the token is not pinned. |
project_id | string | Optionally confine results to one accessible project. |
kinds | string | Optional comma-separated filter: project, task, note, milestone, decision, comment, message, attachment, link, preview, pin. |
limit_per_kind | integer | Maximum hits returned for each kind. Defaults to 8. |
Projects
GET/projects
List projects
Parameters
workspace_id | string | |
visibility | "private" | "public" | |
priority | "low" | "medium" | "high" | |
include_archived | boolean | |
limit | integer | How many rows to return. Defaults to 50. |
cursor | string | The next_cursor from the previous page. |
POST/projects
Create a project
workspace_id is needed only when you belong to more than one and the token is not pinned — otherwise there is exactly one right answer and asking would be pedantry.
Body
namerequired | string | |
description | string | |
color | "blue" | "cyan" | "violet" | "emerald" | "amber" | "rose" | Defaults to blue. |
visibility | "private" | "public" | Defaults to private. |
priority | "low" | "medium" | "high" | Defaults to medium. |
due_date | date | null | |
workspace_id | string | |
GET/projects/{id}
One project, and what you may do with it
Parameters
idrequired | string | Project id. |
PATCH/projects/{id}
Change a project
Parameters
idrequired | string | Project id. |
Body
name | string | |
description | string | |
color | "blue" | "cyan" | "violet" | "emerald" | "amber" | "rose" | Defaults to blue. |
visibility | "private" | "public" | Defaults to private. |
priority | "low" | "medium" | "high" | Defaults to medium. |
due_date | date | null | |
archived | boolean | |
DELETE/projects/{id}
Delete a project
Takes the project's whole history with it, so only the owning team may.
Parameters
idrequired | string | Project id. |
Tasks
GET/projects/{id}/tasks
This project's tasks
Parameters
idrequired | string | Project id. |
kind | "task" | "post" | "all" | Work by default. post returns the project's pinned board notes instead, and all returns both. Defaults to task. |
status | "todo" | "doing" | "done" | |
priority | "high" | "medium" | "low" | |
milestone_id | string | |
assignee | string | A user id, or the literal me. |
due_before | date | Tasks due strictly before this calendar day. |
include_archived | boolean | |
limit | integer | How many rows to return. Defaults to 50. |
cursor | string | The next_cursor from the previous page. |
POST/projects/{id}/tasks
Create a task
Parameters
idrequired | string | Project id. |
Body
titlerequired | string | |
details | string | |
status | "todo" | "doing" | "done" | Defaults to todo. |
priority | "high" | "medium" | "low" | Defaults to medium. |
assignee_id | string | null | |
milestone_id | string | null | |
due_date | date | null | |
kind | "task" | "post" | Create only. post writes a note onto the project board instead of work. Defaults to task. |
pinned | boolean | Create only. Puts the new row's card on the project board. A post always is. |
pin_color | "butter" | "peach" | "rose" | "lilac" | "sky" | "mint" | The paper the board card is drawn on. Defaults to butter. |
GET/tasks
Tasks across every project you can reach
The view an API has that the app does not really offer: 'everything assigned to me that is overdue' is one call here and a walk through the dashboard in a browser.
Parameters
project_id | string | |
workspace_id | string | |
kind | "task" | "post" | "all" | Work by default. post returns the project's pinned board notes instead, and all returns both. Defaults to task. |
status | "todo" | "doing" | "done" | |
priority | "high" | "medium" | "low" | |
milestone_id | string | |
assignee | string | A user id, or the literal me. |
due_before | date | Tasks due strictly before this calendar day. |
include_archived | boolean | |
limit | integer | How many rows to return. Defaults to 50. |
cursor | string | The next_cursor from the previous page. |
GET/tasks/{id}
One task
PATCH/tasks/{id}
Change a task
A field left out is left alone; a field sent as null is cleared. That distinction is the whole reason this is a PATCH — it lets you hand over {"assignee_id": "…"} without first reading the task back to avoid blanking its due date.
Body
title | string | |
details | string | |
status | "todo" | "doing" | "done" | Defaults to todo. |
priority | "high" | "medium" | "low" | Defaults to medium. |
assignee_id | string | null | |
milestone_id | string | null | |
due_date | date | null | |
kind | "task" | "post" | Create only. post writes a note onto the project board instead of work. Defaults to task. |
pinned | boolean | Create only. Puts the new row's card on the project board. A post always is. |
pin_color | "butter" | "peach" | "rose" | "lilac" | "sky" | "mint" | The paper the board card is drawn on. Defaults to butter. |
DELETE/tasks/{id}
Delete a task
POST/tasks/{id}/advance
Move a task one step along
todo → doing → done, and around again from done. What a keyboard shortcut or a chat command actually wants: advancing needs no knowledge of where the task currently is.
GET/tasks/{id}/notes
Notes linked to a task
GET/tasks/{id}/comments
A task's thread
POST/tasks/{id}/comments
Post to the thread
An @name here is resolved to a person or a project agent exactly as it is from the composer in the app — so this can reach a teammate by mail, or start an agent run.
GET/tasks/{id}/attachments
A task's file attachments
POST/tasks/{id}/attachments
Attach files to a task
Multipart only. Up to 10 files per task, 15 MB each, 60 MB total — cumulative with whatever the task already carries, not just this call's batch.
GET/task-attachments/{id}
Download a task attachment
With the same bearer token used to read its task.
Parameters
idrequired | string | Attachment id. |
DELETE/task-attachments/{id}
Delete a task attachment
Parameters
idrequired | string | Attachment id. |
GET/tasks/{id}/links
A task's links
POST/tasks/{id}/links
Add a link to a task
An Open Graph preview is fetched for it automatically, in the background.
DELETE/task-links/{id}
Remove a link from a task
DELETE/comments/{id}
Delete a comment
By the person who wrote it, or by the team that owns the project.
Parameters
idrequired | string | Comment id. |
GET/notes/{id}/tasks
Tasks linked to a note
POST/notes/{id}/tasks
Attach or create a linked task
Send task_id to attach existing work idempotently. Without it, title creates a new task and connects it to this note.
Body
task_id | string | |
title | string | |
details | string | |
status | "todo" | "doing" | "done" | |
priority | "high" | "medium" | "low" | |
assignee_id | string | null | |
milestone_id | string | null | |
due_date | date | null | |
DELETE/notes/{id}/tasks/{taskId}
Remove a note/task connection
Neither the note nor task is deleted.
Parameters
idrequired | string | Note id. |
taskIdrequired | string | Task id. |
Milestones
GET/projects/{id}/milestones
This project's milestones
Parameters
idrequired | string | Project id. |
POST/projects/{id}/milestones
Create a milestone
Parameters
idrequired | string | Project id. |
Body
titlerequired | string | |
date | date | null | |
done | boolean | |
PATCH/milestones/{id}
Change a milestone
Parameters
idrequired | string | Milestone id. |
Body
title | string | |
date | date | null | |
done | boolean | |
pinned | boolean | Pins the milestone to the top of the project's timeline, for everyone who opens it. Nothing is hidden by it — only the order changes. |
DELETE/milestones/{id}
Delete a milestone
Leaves the work planned against it standing, with its milestone set to null — losing a date should not take the tasks with it.
Parameters
idrequired | string | Milestone id. |
Decisions
GET/projects/{id}/decisions
The decision log
Parameters
idrequired | string | Project id. |
POST/projects/{id}/decisions
Record a decision
Parameters
idrequired | string | Project id. |
DELETE/decisions/{id}
Strike an entry from the log
The owning team's to do, and never the author's alone — a log its writers can quietly tidy is not a record.
Parameters
idrequired | string | Decision id. |
Notes
GET/tasks/{id}/notes
Notes linked to a task
GET/projects/{id}/notes
This project's notes
Parameters
idrequired | string | Project id. |
POST/projects/{id}/notes
Create a note
Leave title and content out for a blank note, or provide template_id to copy defaults. Explicit title/content override template values.
Parameters
idrequired | string | Project id. |
Body
title | string | |
content | string | Markdown. |
template_id | string | Accessible project/workspace template to copy. |
GET/projects/{id}/note-templates
Templates available to a project
Project guests see project templates only; owning-team members also see workspace templates.
Parameters
idrequired | string | Project id. |
POST/projects/{id}/note-templates
Create a note template
Parameters
idrequired | string | Project id. |
Body
namerequired | string | |
title | string | |
content | string | Default Markdown. |
scope | "project" | "workspace" | Defaults to project. |
GET/projects/{id}/note-personalization
My favorite and recently visited notes
Private to the authenticated user; visits are ordered newest first and capped at 50.
Parameters
idrequired | string | Project id. |
PATCH/note-templates/{id}
Update a note template
Changes future instantiations only; existing notes remain independent.
Parameters
idrequired | string | Template id. |
Body
name | string | |
title | string | |
content | string | Default Markdown. |
DELETE/note-templates/{id}
Delete a note template
Existing notes created from it remain unchanged.
Parameters
idrequired | string | Template id. |
GET/notes/{id}
One note
PATCH/notes/{id}
Change a note
A field left out is left alone. Writes only the note's stored copy — see the content field's own description on this shape.
Body
title | string | |
content | string | Markdown, replacing the note in full. |
DELETE/notes/{id}
Delete a note
GET/notes/{id}/links
A note's links and backlinks
Returns resolved and unresolved [[Note title]] references written by this note, plus the notes that link back to it.
GET/notes/{id}/comments
A note's anchored discussions
Open threads first, with replies oldest first.
POST/notes/{id}/comments
Comment on selected note text
Body
bodyrequired | string | |
anchorrequired | object | |
GET/notes/{id}/metadata
A note's tags and typed properties
GET/notes/{id}/personalization
My state for one note
PATCH/notes/{id}/personalization
Favorite or unfavorite a note
POST/notes/{id}/visit
Record that I visited a note
Moves the note to the front of this user's project recents without changing shared note state.
GET/projects/{id}/note-views
Saved smart views and their current notes
Parameters
idrequired | string | Project id. |
POST/projects/{id}/note-views
Save a smart view
Parameters
idrequired | string | Project id. |
Body
namerequired | string | |
filterrequired | object | |
PATCH/note-views/{id}
Rename or change a smart view
Parameters
idrequired | string | Smart view id. |
DELETE/note-views/{id}
Delete a smart view
Notes and metadata remain intact.
Parameters
idrequired | string | Smart view id. |
PATCH/note-comments/{id}
Edit, resolve, or reopen an anchored discussion
Only the author may edit body; any project editor may resolve or reopen.
Parameters
idrequired | string | Note comment thread id. |
Body
body | string | |
resolved | boolean | |
DELETE/note-comments/{id}
Delete an anchored discussion
The author or owning team may delete it; replies are deleted with the thread.
Parameters
idrequired | string | Note comment thread id. |
POST/note-comments/{id}/replies
Reply to an anchored discussion
Parameters
idrequired | string | Note comment thread id. |
PATCH/note-comment-replies/{id}
Edit an anchored-discussion reply
Only the reply author may edit it.
Parameters
idrequired | string | Note comment reply id. |
DELETE/note-comment-replies/{id}
Delete an anchored-discussion reply
The reply author or owning team may delete it.
Parameters
idrequired | string | Note comment reply id. |
GET/notes/{id}/tasks
Tasks linked to a note
POST/notes/{id}/tasks
Attach or create a linked task
Send task_id to attach existing work idempotently. Without it, title creates a new task and connects it to this note.
Body
task_id | string | |
title | string | |
details | string | |
status | "todo" | "doing" | "done" | |
priority | "high" | "medium" | "low" | |
assignee_id | string | null | |
milestone_id | string | null | |
due_date | date | null | |
DELETE/notes/{id}/tasks/{taskId}
Remove a note/task connection
Neither the note nor task is deleted.
Parameters
idrequired | string | Note id. |
taskIdrequired | string | Task id. |
Previews
Builds under review, and what people pinned on them.
GET/projects/{id}/previews
This project's previews
Parameters
idrequired | string | Project id. |
POST/projects/{id}/previews
Register a build for review
The review link exists from this moment; nothing reaches it until the snippet is on the page. GET /previews/{id}/review-link returns both.
Parameters
idrequired | string | Project id. |
Body
name | string | Defaults to the URL's hostname. |
urlrequired | uri | Full URL, including https://. |
extra_origins | | Other origins the same build is served from. |
GET/previews/{id}
One preview
Parameters
idrequired | string | Preview id. |
PATCH/previews/{id}
Change a preview
A field left out is left alone. status: "closed" ends the review: the threads stay, the overlay stops being served, and every reviewer key stops resolving.
Parameters
idrequired | string | Preview id. |
Body
name | string | |
url | uri | |
extra_origins | | |
status | "open" | "closed" | |
DELETE/previews/{id}
Delete a preview
Takes every pin on it too. Closing it instead keeps what was said.
Parameters
idrequired | string | Preview id. |
GET/previews/{id}/review-link
The review link, and the snippet it needs
Behind an edit grant rather than on every list: the link is a credential — whoever holds it may comment — for the same reason a project's share token is never returned with the project.
This is also the only followable address a preview has, and the one to use for anything meaning “open the preview”. hosted_url is where the files answer, on an origin that receives no coLab session, so a request there is refused without a ticket — and a build opened directly carries no review overlay, so the notes left on it are invisible. This link mints the ticket and switches the overlay on.
Parameters
idrequired | string | Preview id. |
at | string | A path on the build to open, such as /pricing, so a note left deep in a flow opens on the page it was left on. Defaults to the build's front door. |
POST/previews/{id}/review-link
Rotate the review link
Mints a new link and kills the old one, along with every reviewer key issued against it. What people already wrote stays where it is.
Parameters
idrequired | string | Preview id. |
GET/previews/{id}/threads
The pins on a preview
Newest first, each with its whole conversation.
Parameters
idrequired | string | Preview id. |
status | "open" | "resolved" | open is the list of what this build still owes somebody. |
limit | integer | Defaults to 50. |
GET/preview-threads/{id}
One pin and its conversation
Parameters
idrequired | string | Thread id. |
PATCH/preview-threads/{id}
Resolve a pin, or put it back
Parameters
idrequired | string | Thread id. |
Body
statusrequired | "open" | "resolved" | |
DELETE/preview-threads/{id}
Take a pin down
Parameters
idrequired | string | Thread id. |
GET/preview-threads/{id}/comments
One pin's conversation
Parameters
idrequired | string | Thread id. |
POST/preview-threads/{id}/comments
Answer a reviewer where they asked
The reply appears in the overlay on the build itself, not only in coLab, so the person who pinned it is told without an account and without being emailed.
Parameters
idrequired | string | Thread id. |
POST/preview-threads/{id}/task
Turn a pin into a task
The task quotes the thread rather than only linking to it — a preview URL is the thing here most likely to stop resolving. A pin that is already a task is refused.
Parameters
idrequired | string | Thread id. |
GET/preview-threads/{id}/screenshot
What the reviewer saw
The viewport as their own browser captured it. 404 when there is none.
Parameters
idrequired | string | Thread id. |
Chat and agents
PATCH/chat-messages/{id}
Edit a message you wrote
Only the author, and only a message that has not been deleted. Who it names with @ is worked out again from the new text. A message needs text, so this cannot clear one.
Parameters
idrequired | string | Message id. |
DELETE/chat-messages/{id}
Delete a message
By the person who wrote it, or by the team that owns the project. The text, files, reactions and task links go; the message stays as a deleted one so a reply to it still has something to point at.
Parameters
idrequired | string | Message id. |
POST/chat-messages/{id}/report
Report a message
Reports a message as objectionable. From then on the message is hidden from you, and the operator is told and reviews it within 24 hours. Reporting the same message again answers with the first report. Your own messages, and removed ones, cannot be reported.
Parameters
idrequired | string | Message id. |
Body
reasonrequired | "harassment" | "hate" | "sexual" | "violence" | "spam" | "other" | |
details | string | |
POST/users/{id}/block
Block a person
Their messages, reactions and typing are hidden from you, a direct conversation with them leaves your list, nothing they do notifies you, and neither of you can message the other directly. They are not told. Blocking twice is the same as once.
DELETE/users/{id}/block
Unblock a person
GET/conversations/{id}/mentions
Who `@` can name here
The project's people and its agents (by handle) in a channel — what a composer offers after an @. Empty in a direct chat, where @ names no one.
Parameters
idrequired | string | Conversation id. |
GET/projects/{id}/agents
Agents you can chat with or mention
The name to mention is handle, never name. /projects/{id}/mentions lists these alongside the people, which is the better call when the question is who you can address rather than what agents exist.
Parameters
idrequired | string | Project id. |
GET/projects/{id}/mentions
Everyone and everything you can @mention here
The project's people and its agents in one list, each carrying mention — the literal text to write after the @.
Worth calling before writing a mention rather than after wondering why one did nothing. Mentions are matched against a fixed list when the text is saved, and anything that does not match is kept as ordinary prose: nobody is notified, no agent runs, and nothing reports a problem. A person is named by their display name and an agent by its handle — two different fields, and confusing them fails silently.
Mentioning an agent is what triggers a run, so this is also how work is handed to one. Guests invited to this one project are included; they can be given work here like anybody else.
Mentions resolve in a task's details, a task comment, a note and a chat message. They do not resolve in a reply on a review pin, which stores its body verbatim.
Parameters
idrequired | string | Project id. |
GET/projects/{id}/conversations
This project's chats
Parameters
idrequired | string | Project id. |
POST/projects/{id}/conversations
Open a channel, an agent chat or a direct message
{"kind":"direct","agent_id":"…"} opens your private chat with one agent — sending to it starts the agent automatically. {"kind":"direct","user_id":"…"} opens a direct message with another person on this project — one who can also reply, so the project's owners and editors — and returns the existing one if there is one; each message notifies the other person. Give agent_id or user_id, not both. {"kind":"channel","title":"…"} creates a channel, which wakes agents only when their @handle appears.
Parameters
idrequired | string | Project id. |
Body
kind | "channel" | "direct" | Defaults to channel. |
title | string | For channel. |
agent_id | string | For direct: a chat with an agent. |
user_id | string | For direct: a direct message with a person. |
GET/conversations/{id}/messages
Read a conversation
Parameters
idrequired | string | Conversation id. |
limit | integer | Defaults to 120. |
cursor | string | |
POST/conversations/{id}/messages
Send a message
Send JSON, or multipart form data with the same fields and up to four files. A direct chat wakes its agent; a channel message wakes every enabled agent named with an @handle.
Parameters
idrequired | string | Conversation id. |
Body
body | string | |
parent_id | string | null | Reply to a message. |
task_id | string | null | Attach to a task. |
GET/conversations/{id}/huddles
The call running in a channel, if any
Parameters
idrequired | string | Conversation id. Must be a project channel. |
POST/conversations/{id}/huddles
Start a huddle, or join the one already running
Starting requires write access to the channel, same as posting. Once one is running, anyone who can read the channel may join it — this endpoint answers either way.
Parameters
idrequired | string | Conversation id. Must be a project channel. |
POST/huddles/{id}/join
Join a huddle by id
Parameters
idrequired | string | Huddle id. |
POST/huddles/{id}/leave
Leave a huddle
The huddle itself ends once nobody is left in it.
Parameters
idrequired | string | Huddle id. |
POST/huddles/{id}/end
End a huddle for everyone
Kept to whoever started it, or the workspace that owns the project.
Parameters
idrequired | string | Huddle id. |
GET/link-images/{id}
The picture on a link preview
The image_url of a message's previews entry, with the same bearer token used to read its conversation.
Parameters
idrequired | string | Link preview id. |
GET/chat-attachments/{id}
Download an attachment
With the same bearer token used to read its conversation.
Parameters
idrequired | string | Attachment id. |
Notifications
The inbox: what is addressed at you, and the ambient rest.
GET/notifications
The inbox
Newest first, in two tiers as on the web. direct is what is addressed at you: a mention, a reply, work handed to you, a direct message, a huddle. activity is the ambient rest: a task created or finished, a decision recorded. Each row's sentence arrives already worded in three pieces (text.actor in bold, the quiet text.verb, text.subject in bold), so a new kind reads correctly in an older client; text.actor is null for a folded row, where count leads. url is app-relative. The unread counts for both tiers come back too.
Parameters
tier | "direct" | "activity" | Defaults to direct. |
limit | integer | Defaults to 50. |
workspace_id | string | Defaults to the token's workspace, else the first. |
GET/notifications/counts
Just the unread counts
For a client that polls to keep a badge honest: a few counts, not a hundred rows.
POST/notifications/read-all
Mark the inbox read
One tier, or both when none is given. Changes only what you have seen.
Body
tier | "direct" | "activity" | |
workspace_id | string | |