Interactive API (Swagger UI)
WarmDesk ships with a full Swagger UI at:
http://<your-server>:8080/swagger/index.html
The interactive documentation lists every endpoint, shows request/response schemas, and lets you try requests directly from the browser. Enter a valid JWT Bearer token via the Authorize button to authenticate requests.
Authentication
JWT (user sessions)
Obtain an access token:
curl -s -X POST https://warmdesk.example.com/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"hunter2"}' \
| jq .access_tokenUse the token in subsequent requests:
curl -H "Authorization: Bearer <token>" \
https://warmdesk.example.com/api/v1/projectsAccess tokens expire after 15 minutes
The frontend silently refreshes using the 7-day refresh token
A 401 response from the API signals an expired token
API keys
API keys authenticate CI/CD pipelines and automation scripts. Create a personal API key in Settings → API Keys (full access as you, across all your projects), or a project-scoped key in Project Settings → API Keys (locked to that one project).
Send the key in either header:
X-API-Key: <key>Authorization: ApiKey <key>— for tools that can only set theAuthorizationheader (e.g. Prometheus)
curl -H "X-API-Key: cwk_abc123..." \
https://warmdesk.example.com/api/v1/projectsAPI keys carry the same project permissions as the user who created them, and work on all authenticated endpoints, not just the Ticket API.
A project-scoped key only works on routes that contain its own project’s slug (e.g. /api/v1/projects/{slug}/… or /api/v1/ticket/{slug}/…); anything else returns key is scoped to a specific project.
A card transfer (…/cards/{cardId}/transfer) additionally needs the key to be valid for the target project, so a project key can never write outside its own project; transferring a card needs a personal key.
Keys with the backup global role can trigger backups without a full admin account.
Ticket API
The Ticket API is designed for CI/CD automation and external tools.
All endpoints live under /api/v1/ticket/{projectSlug}/ and use X-API-Key authentication.
The GET endpoints need only viewer access and never change anything; the others need member access (commenting needs viewer access).
| Method | Path | Action |
|---|---|---|
|
| List columns (lanes), left to right |
|
| List cards |
|
| Get one card, with its comments |
|
| Create a card |
|
| Add a comment |
|
| Update a card’s fields (partial) |
|
| Move a card to another column |
|
| Move or copy a card to another project |
Addressing cards and lanes: {cardId} is either the numeric id or the card’s key (ANSI-12, prefix case-insensitive); a card from another project always returns 404.
A card that moved to another project keeps answering to its old key in its new project; asking for it in the old project returns 404 with moved_to (the new key) and project (the new slug), when the key’s user can see that project.
Wherever a lane is expected (create, move), send column_id (number) or column (lane name, case-insensitive), not both.
Read a lane
GET /cards returns open cards ordered by column, then position.
Each card includes column_name and its key (e.g. ANSI-12), plus priority, story_points, time_spent_minutes, due_date, labels, tags, assignees, and epic.
| Query parameter | Type | Notes |
|---|---|---|
| string | Only cards in the column with this name (case-insensitive), e.g. |
| number | Only cards in this column |
| bool |
|
An unknown column returns 400 column not found in project.
curl -s -H "X-API-Key: cwk_abc123..." \
"https://warmdesk.example.com/api/v1/ticket/myproject/cards?column=Backlog" \
| jq '.[] | {key, title, priority, story_points}'Create a card
title and a lane (column_id or column) are required.
Optionally set description, priority (default none), start_date, due_date, and story_points, with the same rules as the update endpoint below.
Invalid values return 400 and nothing is created; unrecognised fields are ignored, so existing callers keep working.
curl -X POST \
-H "X-API-Key: cwk_abc123..." \
-H "Content-Type: application/json" \
-d '{
"title": "Security scan found CVE-2026-1234",
"description": "**Severity:** High\n\nFound in dependency `foo@1.2.3`.",
"column": "Backlog",
"priority": "high"
}' \
"https://warmdesk.example.com/api/v1/ticket/myproject/cards"Returns 201 Created with the new card, including its key.
Update a card
PATCH /cards/{cardId} is a partial update: only fields present in the body change, and a field you leave out keeps its value.
Lane and position go through /move.
| Field | Type | Notes |
|---|---|---|
| string | Must not be empty |
| string | Markdown |
| string | One of |
| boolean |
|
| string or null |
|
| string or null | Same; must not be before |
| integer or null | Non-negative; |
Dates are returned as RFC 3339 timestamps (a plain date is stored as midnight UTC).
Any invalid or unknown field returns 400 and changes nothing; a card from another project returns 404; less than member access returns 403.
The response is the updated card in the same shape as GET /cards/{cardId}, the change is logged in the card’s activity history, and open boards update live.
curl -X PATCH \
-H "X-API-Key: cwk_abc123..." \
-H "Content-Type: application/json" \
-d '{"priority": "medium", "start_date": "2026-09-24"}' \
"https://warmdesk.example.com/api/v1/ticket/myproject/cards/ANSI-4"Add a comment / move a card
# Add a comment
curl -X POST \
-H "X-API-Key: cwk_abc123..." \
-H "Content-Type: application/json" \
-d '{"body": "Deployment to production succeeded. Build #4242."}' \
"https://warmdesk.example.com/api/v1/ticket/myproject/cards/MYPROJ-42/comments"
# Move the card to the Done lane (omit position to append at the end)
curl -X PATCH \
-H "X-API-Key: cwk_abc123..." \
-H "Content-Type: application/json" \
-d '{"column": "Done"}' \
"https://warmdesk.example.com/api/v1/ticket/myproject/cards/MYPROJ-42/move"
# Close it
curl -X PATCH \
-H "X-API-Key: cwk_abc123..." \
-H "Content-Type: application/json" \
-d '{"closed": true}' \
"https://warmdesk.example.com/api/v1/ticket/myproject/cards/MYPROJ-42"Move a card to another project
curl -X POST \
-H "X-API-Key: cwk_abc123..." \
-H "Content-Type: application/json" \
-d '{"target_project": "operations", "column": "Backlog"}' \
"https://warmdesk.example.com/api/v1/ticket/myproject/cards/MYPROJ-42/transfer"Body fields: target_project (slug, required), column_id or column (lane in the target project, required), action (move, the default, or copy) and sub_cards (move, the default, or detach).
The response (201) is the card in the target project; key is its new key.
A move keeps the card itself, so its comments, checklist, attachments, history, links and time spent come along. What changes:
The card gets the next number in the target project (
MYPROJ-42becomes e.g.OPS-7). The old key keeps resolving wherever a key is accepted: the Ticket API, git webhooks, card references and ticket links. The numeric cardidstays the same, but paths are per project, so use the target project’s slug after the move; paths under the old project return404.Labels are matched by name onto the target project’s labels; missing ones are created there with the same colour.
Epic and sprint are cleared, as both belong to the source project.
Assignees and watchers who cannot see the target project are removed.
Sub-cards that move along land in the target lane with the same name as their current lane, or else in the requested lane.
A copy creates a new card with the title, description, priority, due date and tags.
Sessions and personal keys can also use POST /api/v1/projects/{slug}/cards/{cardId}/transfer with target_project_slug, column_id, action and sub_cards.
Time-tracking timer
One running timer per user, kept on the server; stopping it books the time as ordinary time entries.
The warmdesk-timer command-line tool uses these endpoints. They need time tracking enabled for the user and a session or a personal API key (project-scoped keys are refused).
| Method | Path | Action |
|---|---|---|
|
| Running timer, or |
|
| Projects and customers you can start a timer on |
|
| Start ( |
|
| Stop and book; optional |
|
| Discard without booking |
The duration is rounded up to whole 15 minutes and split at midnight into one entry per day, in the timer’s time zone (default: the user’s setting).
Webhooks
Generic webhook (chat notifications)
Post any JSON payload to push a message to a project’s chat channel:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"text":"Deployment to staging succeeded 🚀","username":"CI"}' \
"https://warmdesk.example.com/api/v1/webhooks/<token>"The token is generated in Project Settings → Webhooks.
Git platform webhooks
Register an endpoint in your git host’s project settings to link commits and pull requests to cards.
Card references in commit messages and PR titles (PROJ-42) are parsed automatically.
POST /api/v1/github-webhook/:token # GitHub push, PR, issue events
POST /api/v1/gitlab-webhook/:token # GitLab push, MR, issue events
POST /api/v1/gitea-webhook/:token # Gitea / Forgejo push, PR, issue eventsResponse format
All success responses return JSON. All error responses use a consistent shape:
{ "error": "human readable message" }HTTP status codes follow standard conventions:
200 | OK — resource returned or updated |
201 | Created — resource created; |
204 | No Content — successful delete |
400 | Bad Request — validation error |
401 | Unauthorized — missing or expired token |
403 | Forbidden — insufficient role |
404 | Not Found — resource does not exist or is not accessible |
422 | Unprocessable Entity — semantic validation error |
500 | Internal Server Error — server-side bug |
WebSocket
Project WebSocket
wss://warmdesk.example.com/api/v1/ws/:projectSlug?token=<jwt>Messages are JSON objects {"type": "…", "payload": {…}}.
Common event types:
| A card was created in the project |
| A card field was changed |
| A card was moved to another column |
| A card was soft-deleted |
| A new chat message was posted |
| A new topic was created |
| A user opened the project |
| A user left the project |
User WebSocket
wss://warmdesk.example.com/api/v1/ws/user?token=<jwt>Delivers user-level events: DM messages, @mentions, and card notifications.
timer.changed ({"running": bool, "booked": n}) is sent whenever the user’s time-tracking timer is started, stopped, or discarded from any client; booked is the number of time entries the change created.
Prometheus metrics
GET /api/v1/metricsRequires Authorization: Bearer <token> with the metrics or admin global role.
Returns standard Prometheus text format.