Skip to content

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/" with Content-Type: application/json and 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/ID fields; replace placeholders with real UUIDs from your environment.

REST

GET /health

curl -s "$BASE_URL/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: teamprojectboardstatusesrequests, then use returned IDs in later calls. Your administrator may provide seed or demo data for a sandbox environment.