Skip to main content

REST API

Programmatic access for scripts, CLI tools, and integrations.

tip

The API surfaces metadata and ciphertext — it does not expose plaintext. To work with card content programmatically you need to implement the crypto layer client-side and present the right Workspace / Board Key. A small example CLI is on the roadmap; until then, the test fixtures under tests/integration/ in the source repo are the best reference.


Base URL & auth

  • Base URL: https://kanban.your-domain.com/api
  • Auth: session cookie. Log in once via POST /api/auth/login (browser does it on the login page), then reuse the resulting Set-Cookie for subsequent calls. The cookie is HttpOnly + Secure + SameSite=Strict.

For CLI use without a browser, the recommended pattern is a long-lived session via curl-with-cookies:

# 1. Log in (returns a Set-Cookie that you save)
curl -c cookies.txt -b cookies.txt -X POST \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","authHash":"...","totp":"123456"}' \
https://kanban.your-domain.com/api/auth/login

# 2. Reuse cookies.txt on every subsequent call
curl -b cookies.txt https://kanban.your-domain.com/api/me
warning

authHash is the Argon2id-derived auth hash, not the plaintext password. Deriving it without a browser requires the same Argon2id parameters MSKanban uses (m=64MB, t=3, p=4) plus the per-user salt returned by POST /api/auth/login-init. There is no "send the raw password" shortcut — that's the whole point of the zero-knowledge layer.


Conventions

  • Content-Type: application/json for request and response bodies
  • Error shape:
    { "error": { "code": "BAD_REQUEST", "message": "human-readable" } }
  • Encrypted fields are always strings of the form v1.<nonce>.<ciphertext> (Base64url-encoded). They round-trip unchanged through the API — the server never touches them
  • IDs are CUIDs (c…), opaque, URL-safe, ≤ 64 chars
  • Timestamps are ISO 8601 with offset (2026-05-25T12:34:56.789Z)

Endpoint reference

The full surface is ~56 routes. The OpenAPI 3.1 spec lives in the project repo at docs/api/openapi.yaml and is the authoritative reference. The table below is a curated index.

Auth

MethodPathNotes
POST/auth/registerCreate account with {email, authHash, publicKey, encPrivateKey, encSymmetricKey, encRecoveryBlob}
POST/auth/login{email, authHash, totp?} → session cookie
POST/auth/logoutInvalidate current session
POST/auth/recoveryRecovery-key-based reset
POST/auth/2fa/enrollGenerate TOTP secret
POST/auth/2fa/verifyConfirm enrollment
POST/auth/2fa/disableRemove TOTP
POST/auth/webauthn/registerBegin WebAuthn registration
POST/auth/webauthn/verifyFinish WebAuthn registration
POST/auth/ws-ticketOne-shot ticket for the WebSocket relay

Self

MethodPathNotes
GET/meCurrent user + workspace memberships
GET/me/exportGDPR Art. 15 export (encrypted blobs + metadata)
DELETE/meGDPR Art. 17 delete — crypto-shreds keys immediately, hard-delete after 30 days
GET/me/notificationsRecent assignment / mention notifications

Workspaces

MethodPathNotes
GET / POST/workspacesList own / create
GET / PATCH / DELETE/workspaces/:id
GET / POST/workspaces/:id/membersList / invite (with {userId, encWorkspaceKey} sealed for the invitee)
DELETE/workspaces/:id/members/:userIdRemove member

Boards, columns, cards

MethodPathNotes
GET / POST/workspaces/:wsId/boardsList / create boards in a workspace
GET / PATCH / DELETE/boards/:id
GET / POST/boards/:id/columnsList / create columns
PATCH / DELETE/columns/:idEdit / delete column
POST/columns/:id/cardsCreate card
GET/boards/:id/cardsList cards on a board
GET / PATCH / DELETE/cards/:id
PATCH/cards/:id/move{toColumnId, beforeCardId?, afterCardId?}
POST / DELETE/cards/:id/labelsAttach / detach (idempotent)
POST/cards/:id/assignments / /cards/:id/assignments/:userIdAssign / unassign
PUT/cards/:id/milestone{milestoneId|null}
POST/cards/:id/commentsCreate comment
POST / GET/cards/:id/attachmentsUpload / list attachments

Labels, milestones, templates, custom fields

MethodPathNotes
GET / POST/boards/:id/labels
PATCH / DELETE/labels/:id
GET / POST/boards/:id/milestones?archived=1 includes archived
PATCH / DELETE/milestones/:id
GET / POST/boards/:id/card-templates
DELETE/card-templates/:id
GET / POST/boards/:id/custom-fields
PATCH / DELETE/custom-fields/:id

Comments, checklists

MethodPath
PATCH / DELETE/comments/:id
GET / POST/cards/:id/checklists
PATCH / DELETE/checklists/:id
POST/checklists/:id/items
PATCH / DELETE/checklist-items/:id

Automations, webhooks

MethodPath
GET / POST/boards/:id/automations
PATCH / DELETE/automations/:id
GET / POST/boards/:id/webhooks
PATCH / DELETE/webhooks/:id
GET/webhooks/:id/deliveries
POST/webhooks/deliveries/:id/requeue

Live + health

MethodPathNotes
GET/healthLiveness/readiness — {ok, db, redis}
GET/boards/:id/liveServer-Sent Events stream of board ticks
GET/boards/:id/activityActivity feed (metadata only)

WebSocket relay

Not an HTTP endpoint — a WebSocket upgrade on /api/ws (or directly on port 3001 in dev):

ws://… /api/ws?t=<ticket>

Two room kinds:

  • card:<id> — Yjs Y.Doc for the card description
  • board:<id> — Yjs Awareness for board-level presence

Every payload is XChaCha20-Poly1305 ciphertext under the Board Key. The relay never decrypts — it just routes by room name.


Rate limits

Default sliding-window limits (configurable via env):

GroupLimit
POST /auth/login10/IP/minute + per-account exponential backoff after 3 failures
POST /auth/register5/IP/hour
Card / column / board mutations60/user/minute
Read-only endpoints600/user/minute

A breached limit returns 429 Too Many Requests with a Retry-After header.


Webhooks (outbound)

Configure per board via the UI or POST /api/boards/:id/webhooks. Payload shape:

{
"verb": "CARD_MOVED",
"boardId": "c...",
"cardId": "c...",
"actorId": "c...",
"timestamp": "2026-05-25T12:00:00.000Z",
"metadata": { "fromColumnId": "c...", "toColumnId": "c..." }
}

Headers on each delivery:

X-MSKanban-Event: CARD_MOVED
X-MSKanban-Delivery-Id: <ulid>
X-MSKanban-Signature: t=<unix>,v1=<hmac-sha256-hex>

Verify the signature with HMAC-SHA256 over <t>.<body> using the webhook secret. The t value is also in the signature header to prevent replay — reject requests with a t more than 5 minutes from now.

Retries on 5xx / network error follow an exponential schedule: 30 s, 2 min, 10 min, 1 h, 6 h, 24 h. Anything still failing after 24 h moves to the dead-letter queue.


OpenAPI

The full spec is at docs/api/openapi.yaml in the project repo. Load it in any OpenAPI viewer (Swagger UI, Stoplight, Insomnia) for an interactive reference.