Base URL: http://localhost:7244
All endpoints return JSON: { "data": T } or { "error": "string" }.
Authentication: session cookie (thask_session) or API key (Authorization: Bearer <key>). All endpoints except login/register require authentication.
Backend: Go (Echo v4). Request validation via struct tags.
Create a new account.
// Request
{ "email": "user@example.com", "password": "min8chars", "displayName": "User" }
// Response 201
{ "data": { "id": "uuid", "email": "user@example.com", "displayName": "User" } }Logs in and sets session cookie. Performs session rotation — deletes all previous sessions for the user.
// Request
{ "email": "user@example.com", "password": "..." }
// Response 200
{ "data": { "id": "uuid", "email": "user@example.com", "displayName": "User" } }Returns the current authenticated user.
// Response 200
{ "data": { "id": "uuid", "email": "user@example.com", "displayName": "User" } }Invalidates the session and clears the cookie.
// Response 200
{ "data": { "success": true } }Teams use role-based access control with four levels:
| Role | Level | Capabilities |
|---|---|---|
owner |
4 | Full control: delete team, transfer ownership |
admin |
3 | Manage members, invite, update team settings |
member |
2 | Create projects, edit nodes/edges, batch operations |
viewer |
1 | Read-only access to projects and graphs |
Middleware chain: Auth → TeamAccess (resolves slug, verifies membership, sets role) → RequireRole(minRole).
Create an API key. Maximum 10 keys per user.
// Request
{ "name": "CLI Token", "expiresIn": 90 }
// Response 201
{
"data": {
"id": "uuid",
"name": "CLI Token",
"keyPrefix": "thsk_ab12345",
"key": "thsk_ab1234567890...",
"expiresAt": "2025-06-01T00:00:00Z",
"createdAt": "2025-03-01T00:00:00Z"
}
}Note: The
keyfield is only returned on creation. Store it securely.
| Field | Validation |
|---|---|
name |
Required, 1-100 chars |
expiresIn |
Optional, 1-365 days. Omit for no expiration. |
List your API keys (key hash is never returned).
// Response 200
{ "data": [{ "id": "uuid", "name": "CLI Token", "keyPrefix": "thsk_ab12345", "lastUsedAt": null, "expiresAt": "...", "createdAt": "..." }] }Delete an API key.
// Response 200
{ "data": { "success": true } }List all teams the user is a member of, with their projects.
// Response 200
{ "data": [{ "id": "uuid", "name": "Team", "slug": "team", "projects": [...] }] }Create a new team. The creator becomes owner.
// Request
{ "name": "My Team", "slug": "my-team" }
// Response 201
{ "data": { "id": "uuid", "name": "My Team", "slug": "my-team", ... } }Get a team by slug.
// Response 200
{ "data": { "id": "uuid", "name": "My Team", "slug": "my-team", ... } }Update team name. Requires admin role or above.
// Request
{ "name": "New Team Name" }
// Response 200
{ "data": { "id": "uuid", "name": "New Team Name", "slug": "my-team", ... } }Delete a team. Requires owner role.
// Response 200
{ "data": { "success": true } }List team members. Requires team membership (authorization enforced).
// Response 200
{ "data": [{ "id": "uuid", "userId": "uuid", "role": "owner", "email": "...", "displayName": "..." }] }Invite a user by email. Requires admin role or above. Admins cannot invite as admin or owner.
// Request
{ "email": "invite@example.com", "role": "member" }
// Response 201
{ "data": { "success": true } }| Field | Validation |
|---|---|
email |
Required, valid email |
role |
Optional: admin, member, viewer. Default: member |
Change a member's role. Requires admin role or above. Admins cannot manage other admins.
// Request
{ "role": "admin" }
// Response 200
{ "data": { "success": true } }Remove a member. Requires admin role or above. Cannot remove owner.
// Response 200
{ "data": { "success": true } }Leave a team. Any member can leave. Sole owner must transfer ownership first.
// Response 200
{ "data": { "success": true } }Transfer team ownership. Requires owner role. Target must be a team member.
// Request
{ "userId": "uuid" }
// Response 200
{ "data": { "success": true } }List projects in a team.
// Response 200
{ "data": [{ "id": "uuid", "name": "Project", ... }] }Create a project in a team.
// Request
{ "name": "New Project", "description": "optional" }Get project details. Requires team membership (any role).
// Response 200
{ "data": { "id": "uuid", "teamId": "uuid", "name": "Project", "description": "...", ... } }Update project. Requires member role.
// Request (all fields optional)
{ "name": "Updated Name", "description": "Updated desc" }Delete project. Requires member role.
// Response 200
{ "data": { "success": true } }All node endpoints require project access (verified via ProjectAccess middleware).
Query params: ?type=TASK&status=PASS (optional filters)
// Response 200
{ "data": [{ "id": "uuid", "type": "TASK", "title": "...", "status": "IN_PROGRESS", ... }] }// Request
{
"type": "TASK",
"title": "New Node",
"description": "optional",
"status": "IN_PROGRESS",
"positionX": 100,
"positionY": 200
}
// Response 201
{ "data": { "id": "uuid", ... } }Returns node with connected edges, connected node IDs, and history.
// Response 200
{
"data": {
"id": "uuid", "type": "TASK", "title": "...",
"connectedEdges": [...],
"connectedNodeIds": ["uuid", ...],
"history": [{ "id": "uuid", "action": "updated", "fieldName": "title", ... }]
}
}Updates a node. Records history for each changed field. Triggers waterfall status propagation when status changes.
// Request (all fields optional)
{
"title": "Updated",
"status": "PASS",
"type": "BUG",
"description": "...",
"assigneeId": "uuid",
"tags": ["tag1", "tag2"],
"parentId": "group-uuid | null"
}Deletes the node. If it's a GROUP, children are unparented (preserved).
// Response 200
{ "data": { "success": true } }Batch update node positions (after drag or layout).
// Request
{
"positions": [
{ "id": "uuid", "x": 100, "y": 200, "width": 300, "height": 200 },
{ "id": "uuid", "x": 400, "y": 100 }
]
}Delete multiple nodes. Requires member role.
// Request
{ "ids": ["uuid1", "uuid2"] }
// Response 200
{ "data": { "success": true } }Batch update status for multiple nodes. Requires member role.
// Request
{ "ids": ["uuid1", "uuid2"], "status": "PASS" }
// Response 200
{ "data": [...] }// Response 200
{ "data": [{ "id": "uuid", "sourceId": "uuid", "targetId": "uuid", "edgeType": "depends_on", "label": "" }] }// Request
{ "sourceId": "uuid", "targetId": "uuid", "edgeType": "depends_on", "label": "optional" }Constraints: no self-loops (validated server-side).
// Request
{ "edgeType": "blocks", "label": "updated label" }// Response 200
{ "data": { "success": true } }Get sharing settings and member list. Requires admin role.
// Response 200
{
"data": {
"linkSharing": "viewer",
"shareUrl": "/shared/abc123...",
"members": [
{ "id": "uuid", "projectId": "uuid", "userId": "uuid", "role": "editor", "createdAt": "...", "user": { "id": "uuid", "email": "...", "displayName": "..." } }
]
}
}Enable or disable link sharing. Requires admin role. Disabling clears the share token; re-enabling generates a new one (old links become invalid).
// Request
{ "linkSharing": "viewer" }
// Response 200
{ "data": { "linkSharing": "viewer", "shareUrl": "/shared/abc123..." } }| Value | Description |
|---|---|
off |
Sharing disabled (default) |
viewer |
Anyone with the link can view |
editor |
Anyone with the link can edit |
Invite a user to the project. Requires admin role.
// Request
{ "email": "user@example.com", "role": "editor" }
// Response 201
{ "data": { "success": true } }| Field | Validation |
|---|---|
email |
Required, valid email |
role |
Required: editor or viewer |
Update a project member's role. Requires admin role.
// Request
{ "role": "viewer" }
// Response 200
{ "data": { "success": true } }Remove a project member. Requires admin role.
// Response 200
{ "data": { "success": true } }Public endpoints for accessing shared projects. No authentication required. Rate limited to 5 requests/second.
Get shared project info (limited fields).
// Response 200
{ "data": { "id": "uuid", "name": "Project", "description": "...", "linkSharing": "viewer" } }Get full graph (nodes + edges) for a shared project.
SSE stream for realtime updates on a shared project. Same event types as authenticated SSE.
When linkSharing is editor, these endpoints are available:
| Method | Path | Description |
|---|---|---|
| POST | /api/shared/:shareToken/nodes |
Create node |
| PATCH | /api/shared/:shareToken/nodes/:nodeId |
Update node |
| DELETE | /api/shared/:shareToken/nodes/:nodeId |
Delete node |
| PATCH | /api/shared/:shareToken/nodes/positions |
Batch update positions |
| POST | /api/shared/:shareToken/edges |
Create edge |
| PATCH | /api/shared/:shareToken/edges/:edgeId |
Update edge |
| DELETE | /api/shared/:shareToken/edges/:edgeId |
Delete edge |
Request/response formats are identical to the authenticated project endpoints.
Server-Sent Events stream for realtime updates. Returns text/event-stream.
Event: connected — initial connection confirmation
Event: node.created — a node was created
Event: node.updated — a node was updated
Event: node.deleted — a node was deleted
Event: edge.created — an edge was created
Event: edge.updated — an edge was updated
Event: edge.deleted — an edge was deleted
Event: graph.layout — auto-layout was applied
Event: graph.import — graph was imported
Each event payload:
{ "type": "node.updated", "projectId": "uuid", "data": {...}, "userId": "uuid" }Run server-side auto-layout. Repositions all nodes and auto-sizes GROUPs. Requires member role.
// Request (all fields optional)
{ "algorithm": "dagre" }
// Response 200
{ "data": [{ "id": "uuid", "x": 100, "y": 200, "width": null, "height": null }, ...] }| Param | Default | Options | Description |
|---|---|---|---|
algorithm |
dagre |
dagre, grid |
Layout algorithm |
Query params: ?since=2025-01-01T00:00:00Z&depth=2
Finds changed nodes and their downstream dependencies via bidirectional BFS.
// Response 200
{
"data": {
"changedNodes": [...],
"impactedNodes": [...],
"failNodes": [...],
"impactEdges": [...]
}
}| Param | Default | Description |
|---|---|---|
since |
7 days ago | ISO date — nodes updated after this time |
depth |
2 | BFS depth for downstream search |
Returns project and team counts for the authenticated user.
// Response 200
{ "data": { "teamCount": 3, "projectCount": 7 } }| Middleware | Scope | Description |
|---|---|---|
| CORS | Global | Allows frontend origin with credentials |
| Rate Limiter | Global | 20 requests/second per client |
| Logger | Global | Structured logging via slog |
| Auth | Protected routes | Cookie or API key → user context |
| TeamAccess | /api/teams/:teamSlug/* |
Resolves slug, verifies membership, sets team role |
| ProjectAccess | /api/projects/:projectId/* |
Verifies team membership for the project |
| RequireRole | Specific routes | Enforces minimum role (owner/admin/member) |
| SharedAccess | /api/shared/:shareToken/* |
Token validation, role mapping, 30s cache, anonymous context |
// 400 Bad Request
{ "error": "Title is required" }
// 401 Unauthorized
{ "error": "Authentication required" }
// 404 Not Found
{ "error": "Node not found" }
// 500 Internal Server Error
{ "error": "Internal server error" }All inputs are validated with Go struct tags (validate):
| Endpoint | Validated Fields |
|---|---|
| Register | email (required, email), password (min 8), displayName (required) |
| Login | email (required), password (required) |
| Create Team | name (required), slug (required, alphanum+hyphen) |
| Invite Member | email (required, email), role (oneof: owner/admin/member/viewer) |
| Create Project | name (required) |
| Create Node | type (required), title (required) |
| Update Node | all fields optional, validated when present |
| Batch Positions | positions array with id, x, y required |
| Create Edge | sourceId (required), targetId (required) |
| Update Edge | edgeType and label optional |