API Endpoints¶
Reference for REST and GraphQL operations, with example curl requests. Argument names, return types, and every operation for your deployment are defined by the GraphQL schema exposed at your API base URL—discover it with introspection or your provider’s schema reference.
Conventions¶
- Base URL: Examples use
BASE_URL(set it to your deployment’s API origin). - GraphQL:
POST "$BASE_URL/"withContent-Type: application/jsonand body{"query":"...","variables":{...}}. - Authentication: Send
Authorization: Bearer <Clerk session token>unless the operation is public or optional-auth (see Authentication). - IDs: GraphQL types often use
String/IDfields; replace placeholders with real UUIDs from your environment.
REST¶
GET /health¶
Response: {"status":"ok"}
GraphQL¶
General¶
Query: hello¶
curl -s -X POST "$BASE_URL/" \
-H "Content-Type: application/json" \
-d '{"query":"query { hello }"}'
Teams¶
Query: team(id: String!)¶
Requires authentication and team membership.
Query: teamsForCurrentUser¶
Teams for the signed-in user.
curl -s -X POST "$BASE_URL/" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CLERK_SESSION_TOKEN" \
-d '{"query":"query { teamsForCurrentUser { id name plan slug } }"}'
Query: teamsAll¶
Super admin only — all teams.
Query: teamBySlug(slug: String!)¶
Optional auth — used for public contexts (e.g. public board URLs).
curl -s -X POST "$BASE_URL/" \
-H "Content-Type: application/json" \
-d '{"query":"query($slug: String!) { teamBySlug(slug: $slug) { id name slug } }", "variables":{"slug":"acme"}}'
Mutation: createTeam(input: CreateTeamInput!)¶
Input: name, plan. The team slug is generated automatically from name; collisions append a short random suffix. Creator becomes a team member (owner).
Mutation: updateTeam(id: String!, input: UpdateTeamInput!) / deleteTeam(id: String!)¶
The slug is intentionally not writable through updateTeam. Public board URLs depend on it.
Mutation: adminRenameTeamSlug(id: String!, slug: String!)¶
Super admin only. Renames an existing team slug. Validates the new slug (lowercase letters, numbers, and hyphens, not reserved) and busts the teamBySlug and publicProjectsByTeamSlug caches for both the old and new values. Public-board bookmarks for the old slug stop resolving — there is no automatic redirect.
Team admin (or super admin) only.
Team members¶
Query: teamMembers(teamId: String!) / teamMemberRole(teamId: String!, userId: String!)¶
Mutation: addTeamMember / removeTeamMember / updateTeamMemberRole¶
Use GraphQL introspection (or your provider’s schema reference) for exact argument lists. The current user is always derived from the token for member-changing operations (no userId on unrelated mutations).
Projects¶
Query: project(id: String!)¶
Query: projects(teamId: String!, includeArchived: Boolean)¶
Query: projectBySlug(slug: String!) / projectByTeamSlugAndProjectSlug(teamSlug: String!, projectSlug: String!)¶
Public read when the project is public (optional auth).
curl -s -X POST "$BASE_URL/" \
-H "Content-Type: application/json" \
-d '{"query":"query($t: String!, $p: String!) { projectByTeamSlugAndProjectSlug(teamSlug: $t, projectSlug: $p) { id name slug teamId isPublic } }", "variables":{"t":"acme","p":"product"}}'
Query: publicProjectsByTeamSlug(teamSlug: String!)¶
Summaries for marketing/public listing.
Mutation: createProject / updateProject / deleteProject¶
CreateProjectInput: teamId, name, optional description, isPublic, customDomain. The project slug is generated automatically from name; collisions append a short random suffix. updateProject does not accept slug.
Mutation: adminRenameProjectSlug(id: String!, slug: String!)¶
Super admin only. Renames an existing project slug and busts the public project-list cache for the owning team.
Project members¶
Query: projectMembers / projectMemberRole¶
Mutation: addProjectMember / removeProjectMember / updateProjectMemberRole¶
Boards¶
Boards sit under a project and hold statuses and requests.
Query: board(id: String!)¶
Authenticated; requires project membership.
Query: boardsByProject(projectId: String!)¶
Optional auth when the project is readable publicly.
curl -s -X POST "$BASE_URL/" \
-H "Content-Type: application/json" \
-d '{"query":"query($projectId: String!) { boardsByProject(projectId: $projectId) { id name slug isPublic allowAnonymous isDefault } }", "variables":{"projectId":"YOUR_PROJECT_ID"}}'
Query: boardByProjectAndSlug(projectId: String!, slug: String!)¶
curl -s -X POST "$BASE_URL/" \
-H "Content-Type: application/json" \
-d '{"query":"query($projectId: String!, $slug: String!) { boardByProjectAndSlug(projectId: $projectId, slug: $slug) { id name slug } }", "variables":{"projectId":"YOUR_PROJECT_ID","slug":"feature-requests"}}'
Mutation: createBoard(input: CreateBoardInput!)¶
CreateBoardInput: projectId, name, optional description, isPublic, allowAnonymous. The board slug is generated automatically from name (unique per project); collisions append a short random suffix. Requires project admin (or super admin).
Mutation: updateBoard / deleteBoard¶
updateBoard does not accept slug.
Mutation: adminRenameBoardSlug(id: String!, slug: String!)¶
Super admin only. Renames an existing board slug. Per-project uniqueness is enforced server-side.
Statuses¶
Statuses belong to a board.
Query: statuses(boardId: String!)¶
curl -s -X POST "$BASE_URL/" \
-H "Content-Type: application/json" \
-d '{"query":"query($boardId: String!) { statuses(boardId: $boardId) { id name color position isDefault showOnRoadmap boardId } }", "variables":{"boardId":"YOUR_BOARD_ID"}}'
Query: status(id: String!)¶
Mutation: createStatus(boardId: String!, input: CreateStatusInput!)¶
CreateStatusInput: name, color, position, optional isDefault, showOnRoadmap.
Mutation: updateStatus / deleteStatus¶
Mutation: setDefaultStatus(boardId: String!, statusId: String!)¶
Tags¶
Tags belong to a project.
Query: tags(projectId: String!) / tag(id: String!)¶
Mutation: createTag(projectId: String!, input: CreateTagInput!)¶
Mutation: updateTag / deleteTag¶
Requests (feature requests)¶
Requests belong to a board (boardId on RequestEntity).
Query: request(id: String!)¶
Query: requests(boardId: String!, statusId: String, tagIds: [String!], limit: Int, offset: Int, search: String)¶
Optional filters: statusId, tagIds, limit (default/max enforced server-side), offset, search.
curl -s -X POST "$BASE_URL/" \
-H "Content-Type: application/json" \
-d '{"query":"query($boardId: String!) { requests(boardId: $boardId, limit: 20) { id title statusId voteCount boardId } }", "variables":{"boardId":"YOUR_BOARD_ID"}}'
Mutation: createRequest(input: CreateRequestInput!)¶
CreateRequestInput: boardId, title, optional description, statusId, tagIds, email (anonymous/public flows). No userId argument — the server uses the authenticated user when present.
curl -s -X POST "$BASE_URL/" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CLERK_SESSION_TOKEN" \
-d '{"query":"mutation($input: CreateRequestInput!) { createRequest(input: $input) { id title boardId statusId } }", "variables":{"input":{"boardId":"YOUR_BOARD_ID","title":"Dark mode"}}}'
Mutation: updateRequest / setRequestStatus / lockRequest / deleteRequest¶
Actor is always the authenticated user from context (plus permission checks).
Request tags¶
Query: requestTags(requestId: String!)¶
Mutation: addRequestTag / removeRequestTag¶
Votes¶
The current user is implied from the token — do not pass userId to vote mutations.
Query: hasVoted(requestId: String!) / voteCount(requestId: String!)¶
Mutation: addVote(requestId: String!) / removeVote(requestId: String!)¶
curl -s -X POST "$BASE_URL/" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CLERK_SESSION_TOKEN" \
-d '{"query":"mutation($requestId: String!) { addVote(requestId: $requestId) { id requestId userId } }", "variables":{"requestId":"YOUR_REQUEST_ID"}}'
Comments¶
Query: comments(requestId: String!) / comment(id: String!)¶
Mutation: createComment(requestId: String!, input: CreateCommentInput!, parentId: String)¶
Requires auth. userId is not an argument — the server sets it from context.user.
curl -s -X POST "$BASE_URL/" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CLERK_SESSION_TOKEN" \
-d '{"query":"mutation($requestId: String!, $input: CreateCommentInput!) { createComment(requestId: $requestId, input: $input) { id body userId } }", "variables":{"requestId":"YOUR_REQUEST_ID","input":{"body":"+1 from us"}}}'
Mutation: updateComment / deleteComment / setCommentHidden / setCommentOfficial¶
setCommentOfficial(id: String!, isOfficial: Boolean!) requires project or team admin or owner (or equivalent super-admin access), same as hiding a comment. New comments are always created with isOfficial: false.
Attachments¶
Query: attachments(requestId: String!) / attachment(id: String!)¶
Mutation: createAttachment(requestId: String!, input: CreateAttachmentInput!)¶
Requires auth; no userId argument.
Mutation: deleteAttachment¶
Users¶
Query: me¶
Current user (requires auth).
Query: user(id: String!)¶
Input types (summary)¶
| Input | Fields (high level) |
|---|---|
CreateTeamInput |
name, plan (slug auto-generated) |
CreateProjectInput |
teamId, name, optional description, isPublic, customDomain (slug auto-generated) |
CreateBoardInput |
projectId, name, optional description, isPublic, allowAnonymous (slug auto-generated, unique per project) |
CreateStatusInput |
name, color, position, optional isDefault, showOnRoadmap |
CreateTagInput |
name, color |
CreateRequestInput |
boardId, title, optional description, statusId, tagIds, email |
UpdateRequestInput |
optional title, description, statusId |
CreateCommentInput |
body |
CreateAttachmentInput |
filePath, fileSize, filename, mimeType |
For the exact GraphQL schema, use introspection on {BASE_URL}/ with your preferred GraphQL client.
Trying the examples¶
Create data in order: team → project → board → statuses → requests, then use returned IDs in later calls. Your administrator may provide seed or demo data for a sandbox environment.