REST API reference
The headless API lets scripts and AI agents read and manage a workspace's tasks and notes. Authentication uses API tokens.
Base URL & authentication
The base URL is your deployment's *.convex.site address — the Workspace → API
tokens page shows it in a ready-made snippet. Every request sends the token as a
bearer:
curl -H "Authorization: Bearer yk_..." \
"https://<deployment>.convex.site/api/app/tasks"Errors & rate limits
Errors are JSON with a single error field:
| Status | Meaning |
|---|---|
400 |
Invalid input — the message says what's wrong |
401 |
Missing, invalid, expired, or revoked token |
403 |
Token lacks the required scope |
404 |
Resource not found (or not in this token's workspace) |
429 |
Rate limit exceeded — retry after Retry-After seconds |
Each token gets 120 requests per minute. Error messages are written to be readable by LLMs, so agents can self-correct.
GET /api/app/tasks
Scope: tasks:read. Paginated list of the workspace's tasks.
Query parameters:
projectId— only tasks in this project.includeDone—trueto include tasks in terminal columns (default: excluded).limit— page size, 1–200 (default 50).cursor— cursor from the previous page.
curl -H "Authorization: Bearer $YOKE_TOKEN" \
"$BASE/api/app/tasks?limit=50&includeDone=false"{
"tasks": [
{
"id": "jd7c...",
"title": "Send the invoice to Acme",
"description": null,
"priority": "high",
"priorityValue": 3,
"status": { "name": "To do", "isTerminal": false },
"project": { "id": "jx2a...", "name": "Inbox" },
"assignee": null,
"labels": [{ "id": "jl9k...", "name": "urgent", "color": "red" }],
"dueDate": 1752192000000,
"dueDateString": "2026-07-11",
"createdAt": 1751884800000,
"source": "manual"
}
],
"cursor": "eyJ...",
"isDone": false
}Pagination notes: done-filtering happens after pagination, so a page can be smaller
than limit — keep following cursor until isDone is true. Priority sorting
applies within a page.
GET /api/app/tasks/:id
Scope: tasks:read. One task with its comments and attachments. Returns
{ "task": ... }, or 404 if the task isn't in the token's workspace.
POST /api/app/tasks
Scope: tasks:write. Creates a task; responds 201 with the created task.
Body fields:
title(required) — string.description— string.priority—1–4or"low"/"medium"/"high"/"critical"(4/critical shows as "urgent" in the app). Default: low.projectId— target project (default: Inbox). Get ids from/api/app/meta.dueDate— epoch milliseconds.labelIds— array of label ids.
curl -X POST -H "Authorization: Bearer $YOKE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Review the PR", "priority": "high", "dueDate": 1752192000000}' \
"$BASE/api/app/tasks"PATCH /api/app/tasks/:id
Scope: tasks:write. Partial update — only the fields present in the body change;
sending null clears a clearable field.
title— string (non-empty).description— string, ornullto clear.priority— number or string, as in create.dueDate— epoch ms, ornullto clear.projectId— project id, ornullto clear.labelIds— replaces the whole set; include current labels to keep them.status— a column name, matched case-insensitively ("in progress"). Unknown names return400listing the available columns. Moving to a terminal column marks the task completed.
curl -X PATCH -H "Authorization: Bearer $YOKE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status": "In progress", "priority": "critical"}' \
"$BASE/api/app/tasks/jd7c..."Returns the updated task as { "task": ... }.
POST /api/app/tasks/:id/comments
Scope: tasks:write. Adds a comment authored by the token's owner.
curl -X POST -H "Authorization: Bearer $YOKE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "Deployed to staging, please verify."}' \
"$BASE/api/app/tasks/jd7c.../comments"Responds 201.
GET /api/app/notes
Scope: notes:read. Metadata of the workspace's notes (no content), newest edits
first. Archived notes are excluded. charCount hints how big a note's content is
before fetching it.
{
"notes": [
{
"id": "jn4m...",
"title": "Meeting notes 2026-08-25",
"folderId": null,
"projectId": "jx2a...",
"charCount": 1840,
"createdAt": 1751884800000,
"updatedAt": 1752192000000
}
]
}With ?q=<text> the endpoint switches to full-text search over titles and content
and returns up to 20 hits as { "id", "title", "snippet", "updatedAt" }.
GET /api/app/notes/:id
Scope: notes:read. One note with its full content as markdown. Returns
{ "note": ... } with id, title, contentMarkdown, folderId, projectId,
archived, createdAt, updatedAt — or 404 if the note isn't in the token's
workspace.
POST /api/app/notes
Scope: notes:write. Creates a note; responds 201 with { "id": ... }.
Body fields:
title(required) — string.contentMarkdown— the note body as markdown (headings, lists, bold/italic, links, code blocks).folderId— target folder.projectId— link the note to a project.
curl -X POST -H "Authorization: Bearer $YOKE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Release checklist", "contentMarkdown": "## Steps\n\n- [ ] tag"}' \
"$BASE/api/app/notes"PATCH /api/app/notes/:id
Scope: notes:write. Updates the title and/or content. When contentMarkdown is
present, mode is required:
"append"— adds the markdown to the end, keeping existing content (including images, which markdown can't carry)."replace"— overwrites the whole body. Fetch the note first if you need the old content.
Sending only title renames the note. Every content edit is saved as a revision.
There is no delete — archiving and deleting notes stays in the app.
curl -X PATCH -H "Authorization: Bearer $YOKE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"contentMarkdown": "## Update\n\nShipped v1.2", "mode": "append"}' \
"$BASE/api/app/notes/jn4m..."Returns the updated note as { "note": ... }.
GET /api/app/meta
Scope: tasks:read. The workspace's projects, labels, and statuses — fetch this
first to map names to ids.
{
"projects": [{ "id": "jx2a...", "name": "Inbox", "color": "gray" }],
"labels": [{ "id": "jl9k...", "name": "urgent", "color": "red" }],
"statuses": [
{ "id": "js3f...", "name": "To do", "color": "gray", "isTerminal": false },
{ "id": "js8h...", "name": "Done", "color": "green", "isTerminal": true }
]
}