kaneo Documentation

All you need. Nothing you don't. Open source project management that works for you, not against you.

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy kaneo on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name kaneo -p 5173:5173 --restart always ghcr.io/usekaneo/kaneo:latest
Configured Services & Ports
โš™๏ธ postgres
image: postgres:16-alpine
โš™๏ธ kaneo
image: ghcr.io/usekaneo/kaneo:latest
๐Ÿ”Œ Ports: 5173:5173
docker-compose.yml
yaml (docker-compose.yml)
services:
  postgres:
    image: postgres:16-alpine
    env_file:
      - .env
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-kaneo}
      POSTGRES_DB: ${POSTGRES_DB:-kaneo}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", 'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"']
      interval: 10s
      timeout: 5s
      retries: 5

  kaneo:
    image: ghcr.io/usekaneo/kaneo:latest
    ports:
      - "5173:5173"
    env_file:
      - .env
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-Y", "off", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5173/api/health"]
      interval: 30s
      timeout: 5s
      start_period: 60s
      retries: 3

#  Optional for HA Setup
#  redis:
#    image: valkey/valkey
#    ports:
#      - "6379:6379"

volumes:
  postgres_data:

Technical Documentation: usekaneo/kaneo

โ„น๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference

Overview (`README.md`)

Built for your team's work

  • See work your way: Board, List, Calendar, and Gantt views, plus a backlog, filters, and search.
  • Keep task context together: descriptions, attachments, subtasks, dependencies, labels, comments, and time tracking.
  • Work with your team: shared workspaces, invitations, custom roles, live updates, and public project views.
  • Shape your workflow: custom columns and fields, with rules that move tasks when repository activity changes.
  • Stay informed: in-app notifications, email, ntfy, Gotify, and personal webhooks.
  • Connect other tools: GitHub and Gitea, chat integrations, project webhooks, a REST API, a command-line client, and MCP for AI assistants.

Installation

Contributing

Code, translations, documentation, and bug reports are welcome. Start with the contributing guide and local development setup. Pay particular attention to contribution eligibility.

Join us on Discord, report bugs in GitHub Issues, or submit and vote on feature requests at Kaneo Feedback.

Sponsors

Kaneo is open source. If you find it useful, consider sponsoring the project to help support ongoing development.

Partners

This project is tested with BrowserStack.

Community sponsors

License

MIT License - see LICENSE for details.



Overview (`sentry/README.md`)

Sentry

This directory holds Sentry configuration as code, so the alert rules and dashboards that Kaneo depends on are version-controlled and reproducible.

Contents

  • alerts.json โ€” alert rules. Six rules cover first-seen issues, error spikes, missed cron check-ins, slow p95 latency, and MCP error rate. Both kaneo-api and kaneo-web are covered.
  • dashboards.json โ€” four Kaneo-specific dashboards (Backend, Frontend, MCP, Cron Monitors) with widgets for the metrics above. The prebuilt Sentry templates are left untouched.

What's NOT here yet

  • Cron monitor alert. The cron jobs in apps/api/src/scheduler/index.ts call Sentry.captureCheckIn with stable slugs (due-date-reminders, project-webhook-reminders, seat-reconciliation, trial-reminders). The Sentry monitors are auto-created the first time each job runs after the next deploy. Once they exist, the next run of provision-sentry-alerts.sh will resolve the slugs and create the cron-missed workflow.

Applying the alerts

The script lives at scripts/provision-sentry-alerts.sh. It reads sentry/alerts.json, finds existing workflows by name, and post-or-puts so edited specs propagate.

bash
# 1. Generate a Sentry auth token (admin scope)
#    https://sentry.io/settings/account/api/auth-tokens/
#    Scope: org:write (or alerts:write)

# 2. Apply
SENTRY_API_TOKEN=sntrys_... ./scripts/provision-sentry-alerts.sh

# Dry-run first to see what would be created or updated
SENTRY_API_TOKEN=sntrys_... ./scripts/provision-sentry-alerts.sh --dry-run

The script targets the EU region (https://de.sentry.io). If Kaneo ever moves to the US region, change the region field in alerts.json (or override SENTRY_API_BASE).

If you rename an alert in alerts.json, the old name stays in Sentry alongside the new one. Rename alerts only when you intend to delete the old one manually.

Option B: manual setup via the Sentry UI

For each rule in alerts.json, walk through Settings > Alerts > Create Alert. The JSON is human-readable enough to drop in to the Sentry UI form fields. Tables of the conditions are at the top of alerts.json; the _comment_* keys are stripped before applying.

After applying

The script creates the rules but does NOT wire up notifications. Each rule lands in Sentry with an empty actions array so you can choose the right channel per rule. To finish setup:

  1. Settings > Alerts > [rule name] > Actions
  2. Pick a notification target. Recommended:
    • Slack #engineering for spike/latency/cron-missed
    • Email to on-call for new issues and MCP errors
  3. Save.

Once notifications are wired, the rules start firing based on the events flowing through Sentry.captureException (anywhere in the codebase) and Sentry.captureCheckIn (the four cron jobs).

Schema notes

Two rule families:

  • Issue rules (kind: "issue") โ€” trigger on event condition types (first_seen_event, regression_event, etc.). One JSON entry per rule.
  • Metric rules (kind: "metric") โ€” trigger on aggregate conditions (count, p95, failure_rate). Each entry creates a Detector (the metric source) and a Workflow (the alert) and links them via detectorIds.

Applying the dashboards

scripts/provision-sentry-dashboards.sh reads sentry/dashboards.json and creates-or-updates each dashboard by title. The script preserves any filter settings (projects, environment, period) the maintainer has set in the Sentry UI by reading the current dashboard first and only swapping the widgets array.

bash
SENTRY_API_TOKEN=sntrys_... ./scripts/provision-sentry-dashboards.sh
SENTRY_API_TOKEN=sntrys_... ./scripts/provision-sentry-dashboards.sh --dry-run

The prebuilt Sentry dashboards (Backend Overview, Frontend Overview, etc.) are not touched. Kaneo-specific dashboards are created with the Kaneo: prefix.

Schema notes

Alerts

Two rule families:

  • Issue rules (kind: "issue") โ€” trigger on event condition types (first_seen_event, regression_event, etc.). One JSON entry per rule.
  • Metric rules (kind: "metric") โ€” trigger on aggregate conditions (count, p95, failure_rate). Each entry creates a Detector (the metric source) and a Workflow (the alert) and links them via detectorIds.

The cron-missed-check-in rule is special: it doesn't create a Detector because the cron monitors already exist. The script resolves the slug strings in detector_slugs to numeric IDs via GET /api/0/organizations/{org}/monitors/. If the cron jobs haven't run yet, the monitors don't exist and the script will skip with a warning.

Dashboards

Each dashboard entry has:

  • title โ€” dashboard name
  • widgets โ€” array of { title, displayType, interval, queries } objects
  • queries โ€” array of { fields, aggregates, columns, query, orderby, limit }
    • fields โ€” columns to display
    • aggregates โ€” aggregate functions to compute (e.g., count(), p95(transaction.duration))
    • columns โ€” group-by fields
    • query โ€” Sentry search filter (e.g., event.type:error project:kaneo-api)
    • orderby โ€” sort field (e.g., -count())
    • limit โ€” required on every query; max is 10. The script uses 10 for all queries.

Performance metrics use event.type:transaction is_transaction:true (the dataset migration removed dataset: transactions). Error metrics use event.type:error. Cron check-in events use event.type:transaction monitor.check_in_id:*.

The Sentry API surface used is documented at:


Overview (`apps/docs/README.md`)

Kaneo documentation

The docs are authored in MDX and rendered by Mintlify. docs.json owns navigation; openapi.json supplies the generated API reference.

Preview and validate

From apps/docs:

bash
pnpm dlx mint dev
pnpm dlx mint validate
pnpm dlx mint broken-links

The preview runs at http://localhost:3000. Use --port 3107 if that port is busy. Search in the local preview may require a Mintlify login.

Organization

  • Using Kaneo: first project, task views, team management, account settings.
  • Self-hosting: installation, configuration, sign-in, backups, upgrades, troubleshooting.
  • Integrations: repositories, team channels, webhooks, and MCP.
  • Developers: first API request, authentication, protocol details, endpoint reference.

Existing core/ page paths are retained so external links keep working. Put a page in the navigation where readers expect to find it rather than moving its URL for cosmetic consistency.

See CONTRIBUTING.md for writing and image guidelines. Product captures and regeneration instructions live in images/product/README.md.


Overview (`apps/mobile/README.md`)

Kaneo Mobile

The Kaneo app for Android and iOS, built with Expo and React Native. Works with Kaneo Cloud (cloud.kaneo.app) and self-hosted instances.

Features

  • Sign in with email and password, a one-time email code, an API key, or Continue in browser (device authorization). The browser flow covers GitHub, Google, Discord, OIDC/SSO and anything else the server supports.
  • Workspaces: switch, create, rename, describe, leave, delete.
  • Members: list, invite with a role, resend or cancel invitations, change roles, remove. Accept or decline your own invitations.
  • Projects: list with progress, create, rename, change the key, make public, archive or restore, move to another workspace, delete.
  • Boards: swipe between columns, or use the list view. Show the backlog and archived tasks. Filter by text, assignee, priority, label or overdue. Long-press a card to move it, change its priority or assignee, or send it to the top or bottom of its column.
  • Columns: add, rename, reorder, mark as done, delete.
  • Tasks: create with every field (including custom fields), edit the title, Markdown description (with preview and image upload), status, priority, assignee, start and due dates, and labels (you can create labels inline). Duplicate, move to another project, share, copy the link, delete.
  • Subtasks and relations: add subtasks, check them off, set or change the parent task, link related or blocking tasks.
  • Links: attach URLs; synced integration links are shown read-only.
  • Custom fields: text, number, date, checkbox, dropdown, multi-select. Define them in project settings.
  • Time tracking: start/stop a timer, log time manually, edit your entries.
  • Comments and activity: Markdown comments with @mentions and image attachments; edit and delete your own comments; full activity timeline.
  • My tasks, Inbox (notifications: mark read, mark all read, clear) and the workspace activity feed.
  • Search across tasks, projects and comments.
  • Account: name, profile photo, password, notification delivery settings (email, ntfy, Gotify, webhook, event types, due-date reminders).
  • Live updates over Kaneo's WebSockets while the app is open, and phone notifications from a background check (Kaneo has no mobile push service). Android runs the check about every 15 minutes; iOS decides when to run it, so it can be less frequent.

Left out on purpose

These are admin or desktop tasks that don't fit a phone: integration setup (GitHub, GitLab, Gitea, Slack, Discord, Telegram, Mattermost, webhooks) and sync rules, workflow rules, task import/export, calendar feeds, instance administration, custom role editing, per-member project access, billing, API key management, MCP, project icons and backgrounds, and Gantt/calendar views. They are still available on the web.

Connecting

  • Kaneo Cloud: tap Kaneo Cloud.
  • Self-hosted: enter the address you open Kaneo at, e.g. kaneo.example.com or http://192.168.1.20:1337. If your web app and API are on different hosts, enter the API address. http:// is allowed for LAN/VPN servers on both platforms.

Continue in browser uses the device-authorization client kaneo-cli, which Kaneo accepts by default. If an instance sets DEVICE_AUTH_CLIENT_IDS, it must include kaneo-cli for this option to work. Password, email code and API key sign-in work regardless.

On Kaneo Cloud, creating an account and requesting an email code are protected by a browser captcha, so the app opens the website for sign-up; password sign-in and browser sign-in work in the app.

Credentials are stored with expo-secure-store: in the iOS Keychain, and encrypted with a key held in the Android Keystore.

Development

Install dependencies from the repository root with pnpm install. The app uses native modules, so run it as a development build rather than in Expo Go:

bash
pnpm --filter @kaneo/mobile android   # needs JDK 17 and the Android SDK
pnpm --filter @kaneo/mobile ios       # needs macOS, Xcode and CocoaPods
pnpm --filter @kaneo/mobile start     # Metro only, for an already installed development build

The ios/ and android/ folders are generated from app.json and the config plugins in plugins/ (continuous native generation) and are not committed. Change native settings there, then regenerate with pnpm --filter @kaneo/mobile exec expo prebuild --clean.

Checks:

bash
pnpm --filter @kaneo/mobile lint
pnpm --filter @kaneo/mobile typecheck
pnpm --filter @kaneo/mobile test

Release builds

The Android APK workflow (.github/workflows/android.yml) builds a release APK and uploads it as a workflow artifact. It runs on pull requests that touch apps/mobile and can be started by hand. Bump version and android.versionCode in app.json for each APK you hand out; Android only installs an update with a higher versionCode.

Release builds are signed with the key named by KANEO_ANDROID_KEYSTORE, KANEO_ANDROID_KEYSTORE_PASSWORD, KANEO_ANDROID_KEY_ALIAS and KANEO_ANDROID_KEY_PASSWORD (see plugins/with-release-signing.js), and with the debug key when those are unset. An APK only installs over one signed with the same key, so create the release key once and keep it safe:

bash
keytool -genkeypair -v -keystore kaneo-release.keystore -alias kaneo \
  -keyalg RSA -keysize 4096 -validity 10000
base64 -i kaneo-release.keystore | pbcopy   # paste into the ANDROID_KEYSTORE_BASE64 secret

CI reads the repository secrets ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS and ANDROID_KEY_PASSWORD. Locally, export the KANEO_ANDROID_* variables and run pnpm --filter @kaneo/mobile exec expo run:android --variant release. For iOS, build with pnpm --filter @kaneo/mobile exec expo run:ios --configuration Release.

Project layout

text
src/app/         Expo Router screens; every file is a route
src/components/  shared UI (sheets, pickers, Markdown, avatars, toasts)
src/features/    screen-specific pieces for the home tabs, board and task
src/lib/         API client and types, auth, session storage, realtime, background notifications
src/theme/       Kaneo design tokens, light and dark
plugins/         local Expo config plugins

Overview (`charts/kaneo/README.md`)

Kaneo Helm Chart

This Helm chart deploys Kaneo - open source project management that works for you, not against you.

Introduction

This chart bootstraps a Kaneo deployment on a Kubernetes cluster using the Helm package manager. It deploys both the API backend and Web frontend components, along with a PostgreSQL database, with optional ingress or Gateway API resources.

Prerequisites

  • Kubernetes 1.23+
  • Helm 3.2.0+
  • PV provisioner support in the underlying infrastructure (if persistence is enabled)

Quick Start

Basic Installation

Install directly from GHCR:

bash
helm install kaneo oci://ghcr.io/usekaneo/charts/kaneo \
  --namespace kaneo \
  --create-namespace
# Access locally
kubectl port-forward svc/kaneo-kaneo 5173:5173 -n kaneo

Open http://localhost:5173 and you're ready to go.

Production Setup with Ingress

For real deployments, you'll want proper ingress:

bash
helm install kaneo oci://ghcr.io/usekaneo/charts/kaneo \
  --namespace kaneo \
  --create-namespace \
  --set ingress.enabled=true \
  --set ingress.className=nginx \
  --set "ingress.hosts[0].host=pm.yourcompany.com"

Production Setup with Gateway API

If your cluster already has Gateway API CRDs and a Gateway configured, you can expose Kaneo with an HTTPRoute:

bash
helm install kaneo oci://ghcr.io/usekaneo/charts/kaneo \
  --namespace kaneo \
  --create-namespace \
  --set gateway.enabled=true \
  --set "gateway.parentRefs[0].name=main-gateway" \
  --set "gateway.parentRefs[0].namespace=gateway-system" \
  --set "gateway.parentRefs[0].sectionName=https" \
  --set "gateway.hostnames[0]=pm.yourcompany.com"

Installing the Chart

To install the published chart with the release name my-kaneo:

bash
helm install my-kaneo oci://ghcr.io/usekaneo/charts/kaneo

To install from a local checkout instead:

bash
helm install my-kaneo ./charts/kaneo

The command deploys Kaneo on the Kubernetes cluster with default configuration. The Parameters section lists the parameters that can be configured during installation.

Uninstalling the Chart

To uninstall/delete the my-kaneo deployment:

bash
helm uninstall my-kaneo

Parameters

Global parameters

Name Description Value
nameOverride String to partially override the fullname template (will maintain the release name) ""
fullnameOverride String to fully override the fullname template ""
replicaCount Number of replicas (ignored if autoscaling is enabled) 1
enableServiceLinks Controls Pod's enableServiceLinks field false
topologySpreadConstraints Controls Pod's topologySpreadConstraints field []

Autoscaling parameters

Name Description Value
autoscaling.enabled Enable autoscaling for the deployment false
autoscaling.minReplicas Minimum number of replicas 1
autoscaling.maxReplicas Maximum number of replicas 10
autoscaling.targetCPUUtilizationPercentage Target CPU utilization percentage 80
When CPU autoscaling is enabled, set kaneo.resources.requests.cpu; Kubernetes cannot calculate CPU utilization without a CPU request.

PostgreSQL Database parameters

Name Description Value
postgresql.enabled Deploy PostgreSQL as part of this chart true
postgresql.image.repository PostgreSQL image repository postgres
postgresql.image.tag PostgreSQL image tag 16-alpine
postgresql.image.pullPolicy PostgreSQL image pull policy IfNotPresent
postgresql.auth.database PostgreSQL database name kaneo
postgresql.auth.username PostgreSQL username kaneo_user
postgresql.auth.password PostgreSQL password kaneo_password
postgresql.auth.existingSecret Name of existing secret containing PostgreSQL credentials ""
postgresql.persistence.enabled Enable persistence for PostgreSQL data true
postgresql.persistence.size PostgreSQL PVC size 8Gi
postgresql.persistence.storageClass PostgreSQL PVC storage class ""
postgresql.persistence.accessMode PostgreSQL PVC access mode ReadWriteOnce
postgresql.service.type PostgreSQL service type ClusterIP
postgresql.service.port PostgreSQL service port 5432
postgresql.resources Resource requests and limits for PostgreSQL container {}

Kaneo application parameters

Name Description Value
kaneo.image.repository Kaneo image repository ghcr.io/usekaneo/kaneo
kaneo.image.tag Kaneo image tag. Defaults to Chart.appVersion when empty ""
kaneo.image.pullPolicy Kaneo image pull policy IfNotPresent
kaneo.service.type Kaneo service type ClusterIP
kaneo.service.port Kaneo service port 5173
kaneo.service.targetPort Kaneo container port 5173
kaneo.env Environment variables for the Kaneo container See values.yaml
kaneo.env.clientUrl Public URL of the Kaneo instance. Required for every deployment; sets KANEO_CLIENT_URL. Helm rejects missing values and URLs with credentials, paths, queries or fragments before creating resources. Note: this key is case-sensitive (clientUrl, not clientURL). ""
kaneo.env.corsOrigins Allowed CORS origins as a comma-separated string or YAML list []
kaneo.env.authSecret Required Better Auth secret (minimum 32 characters), stored in a chart-managed Secret; ignored if existingSecret is enabled ""
kaneo.env.existingSecret.enabled Whether to use an existing secret for AUTH_SECRET false
kaneo.env.existingSecret.name Name of the existing secret containing AUTH_SECRET ""
kaneo.env.existingSecret.key Key in the existing secret that contains AUTH_SECRET auth-secret
kaneo.env.disableRegistration Disable new user registration false
kaneo.env.disablePasswordRegistration Disable password-based account creation while keeping social/OIDC registration available false
kaneo.env.disableEmailOtpSignIn Use email/password sign-in instead of verification codes when SMTP is configured false
kaneo.env.database.external.enabled Use external PostgreSQL database (set postgresql.enabled to false) false
kaneo.env.database.external.host External PostgreSQL host ""
kaneo.env.database.external.port External PostgreSQL port 5432
kaneo.env.database.external.database External PostgreSQL database name kaneo
kaneo.env.database.external.username External PostgreSQL username kaneo_user
kaneo.env.database.external.password External PostgreSQL password ""
kaneo.env.database.external.existingSecret.enabled Use an existing secret for the external database connection URI false
kaneo.env.database.external.existingSecret.name Name of the secret containing the database connection URI ""
kaneo.env.database.external.existingSecret.passwordKey Key in the secret whose value is a full PostgreSQL connection URI postgres_uri
kaneo.extraEnv Additional Kubernetes EnvVar entries appended to the Kaneo container []
kaneo.extraEnvFrom Additional Kubernetes EnvFromSource entries appended to the Kaneo container []
kaneo.resources Resource requests and limits for the Kaneo container (optional, disabled by default) {}
podSecurityContext Security context applied at the Pod level {}
kaneo.securityContext Security context applied at the Kaneo container level {}

Ingress parameters

Name Description Value
ingress.enabled Enable ingress false
ingress.className Ingress class name ""
ingress.annotations Ingress annotations {}
ingress.hosts Ingress hosts configuration See values.yaml
ingress.tls Ingress TLS configuration []

Gateway API parameters

Name Description Value
gateway.enabled Enable Gateway API HTTPRoute creation false
gateway.annotations Annotations added to the HTTPRoute {}
gateway.labels Extra labels added to the HTTPRoute {}
gateway.parentRefs Parent Gateway listener references for attaching the HTTPRoute []
gateway.hostnames Hostnames exposed by the HTTPRoute []
gateway.rules Gateway API routing rules mapping paths to the chart services See values.yaml

Configuration Examples

Minimal Configuration

yaml
# values.yaml
kaneo:
  env:
    authSecret: "your-secure-auth-secret-at-least-32-characters"
    clientUrl: "https://your-domain.com"
ingress:
  enabled: true
  className: nginx
  annotations: {}
  hosts:
    - host: your-domain.com
      paths:
        - path: /
          pathType: Prefix
          service: kaneo
          port: 5173

Production Configuration with TLS

yaml
# values.yaml
replicaCount: 1
# PostgreSQL configuration
postgresql:
  auth:
    password: "your-secure-db-password"
  persistence:
    size: 20Gi
    storageClass: "managed-premium"
  resources:
    limits:
      cpu: 500m
      memory: 512Mi
    requests:
      cpu: 100m
      memory: 128Mi
# Kaneo configuration
kaneo:
  resources:
    limits:
      cpu: 1000m
      memory: 1Gi
    requests:
      cpu: 200m
      memory: 256Mi
  env:
    authSecret: "your-secure-auth-secret-at-least-32-characters"
    clientUrl: "https://your-domain.com"
ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  hosts:
    - host: your-domain.com
      paths:
        - path: /
          pathType: Prefix
          service: kaneo
          port: 5173
  tls:
    - secretName: kaneo-tls
      hosts:
        - your-domain.com

Production Configuration with Traefik

If you're running Traefik as your ingress controller (common in self-hosted clusters), configure the ingress directly rather than through the chart's built-in ingress support:

yaml
# values.yaml
kaneo:
  env:
    authSecret: "your-secure-auth-secret-at-least-32-characters"
    clientUrl: "https://kaneo.your-domain.com"
yaml
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: kaneo-ingress
  namespace: kaneo
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: traefik
  rules:
    - host: kaneo.your-domain.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: kaneo-kaneo
                port:
                  number: 5173
  tls:
    - hosts:
        - kaneo.your-domain.com
      secretName: kaneo-tls

Using External PostgreSQL Database

If you prefer to use an external PostgreSQL database instead of the bundled one:

yaml
# values.yaml
# Disable bundled PostgreSQL
postgresql:
  enabled: false
kaneo:
  env:
    authSecret: "your-secure-auth-secret-at-least-32-characters"
    database:
      external:
        enabled: true
        host: "your-postgres-host.com"
        port: 5432
        database: "kaneo"
        username: "kaneo_user"
        password: "your-db-password"

Using an Existing Secret for Sensitive Data

For production environments, it's recommended to store sensitive data like the auth secret and database credentials in Kubernetes Secrets:
When postgresql.auth.existingSecret is used with bundled PostgreSQL, the password is expanded by Kubernetes into DATABASE_URL at runtime and must be URL-safe. If your password contains reserved URI characters such as @, :, /, #, %, or spaces, use an external database and provide the complete connection URI through kaneo.env.database.external.existingSecret.

bash
# Create a Secret for sensitive data
kubectl create secret generic kaneo-secrets \
  --namespace kaneo \
  --from-literal=auth-secret="your-secure-auth-secret-at-least-32-characters" \
  --from-literal=postgres-password="your-secure-db-password"

Then reference these secrets in your values:

yaml
# values.yaml
postgresql:
  auth:
    existingSecret: "kaneo-secrets"
    secretKeys:
      userPasswordKey: "postgres-password"
kaneo:
  env:
    existingSecret:
      enabled: true
      name: "kaneo-secrets"
      key: "auth-secret"

Using an Existing Secret with External PostgreSQL

When using an external database, the secret must contain a full PostgreSQL connection URI rather than just the password. Any special characters in the password (such as @, :, /) must be percent-encoded.

bash
# Create the secret with a full connection URI
kubectl create secret generic kaneo-secrets \
  --namespace kaneo \
  --from-literal=auth-secret="your-secure-auth-secret-at-least-32-characters" \
  --from-literal=postgres_uri="postgresql://kaneo_user:your-password@your-host:5432/kaneo"
yaml
# values.yaml
postgresql:
  enabled: false

kaneo:
  env:
    clientUrl: "https://kaneo.your-domain.com"
    existingSecret:
      enabled: true
      name: "kaneo-secrets"
      key: "auth-secret"
    database:
      external:
        enabled: true
        host: "your-postgres-host.com"
        port: 5432
        database: "kaneo"
        username: "kaneo_user"
        password: ""
        existingSecret:
          enabled: true
          name: "kaneo-secrets"
          passwordKey: postgres_uri

Database Management

PostgreSQL Configuration

The chart deploys PostgreSQL 16 (Alpine) by default with the following configuration:

  • Database name: kaneo
  • Username: kaneo_user
  • Default password: kaneo_password (change this in production!)
  • Persistent storage: 8Gi (configurable)
    The bundled PostgreSQL deployment is intended for development, trials, and small self-hosted installs. For production environments, use an external managed PostgreSQL database by setting postgresql.enabled=false and configuring kaneo.env.database.external.
    Bundled PostgreSQL credentials are only applied when PostgreSQL initializes an empty data directory. If a PVC already exists, changing postgresql.auth.password or postgresql.auth.existingSecret updates the Pod environment but does not rotate the password inside the existing database. For local retesting with a new password, uninstall the release and delete the test PVC before reinstalling:
bash
helm uninstall kaneo -n kaneo-test
kubectl delete pvc kaneo-postgresql-data -n kaneo-test --ignore-not-found

To preserve data, rotate the password inside PostgreSQL first, then update the Helm values to match.

Backup and Recovery

For production deployments, consider implementing regular database backups:

bash
# Example backup command
kubectl exec -it deployment/my-kaneo-postgresql -- pg_dump -U kaneo_user kaneo > kaneo-backup.sql

Migration from SQLite

If you're migrating from a previous SQLite-based installation, you'll need to:

  1. Export your data from SQLite
  2. Deploy the new PostgreSQL-based chart
  3. Import your data into PostgreSQL
    Contact the Kaneo community on Discord for migration assistance.

Troubleshooting

"invalid origin" error on login

Kaneo's API validates the Origin header on every request against KANEO_CLIENT_URL. The chart now requires clientUrl before rendering. On older deployments, an unset or incorrect value can cause login failures.

Set clientUrl to the URL users access Kaneo from:

yaml
kaneo:
  env:
    clientUrl: "https://kaneo.your-domain.com"

Note: The key is clientUrl (camelCase). clientURL (all-caps) does not configure this value; Helm reports the missing required clientUrl.

Pods crash immediately after upgrading to use `existingSecret`

If pods enter CrashLoopBackOff after switching to an existing secret for the external database, the connection URI in the secret is likely wrong. Check what the pod is actually using:

bash
kubectl get secret kaneo-secrets -n kaneo \
  -o jsonpath='{.data.postgres_uri}' | base64 -d

Common causes:

  • Wrong password
  • Password contains special characters that need percent-encoding (e.g. p@ss โ†’ p%40ss)
  • Incorrect host, port, or database name in the URI

PodSecurity warnings on install/upgrade

On clusters with Pod Security Admission enabled, you may see warnings like would violate PodSecurity "restricted:latest". The chart exposes podSecurityContext and kaneo.securityContext to address these. See the Security section for recommended values.

Architecture

This chart deploys the following components:

  1. Kaneo application: Serves the web UI and API from the combined Kaneo image
  2. PostgreSQL Database: Stores all application data with proper relational integrity
    The Kaneo application and PostgreSQL run in separate pods for resource isolation and simpler database lifecycle management.

Production Environment

For production deployments, you should:

  1. Set secure values for AUTH_SECRET and PostgreSQL passwords
  2. Use an Ingress controller to expose the application
  3. Configure TLS for secure access
  4. Set appropriate resource limits and requests
  5. Enable persistent storage with appropriate storage classes
  6. Consider using external PostgreSQL for better scalability
yaml
ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  hosts:
    - host: your-domain.com
      paths:
        - path: /
          pathType: Prefix
          service: kaneo
          port: 5173
  tls:
    - secretName: kaneo-tls
      hosts:
        - your-domain.com

Using Gateway API

As an alternative to Ingress, the chart can create an HTTPRoute for the Kubernetes Gateway API:

yaml
# values.yaml
gateway:
  enabled: true
  parentRefs:
    - name: main-gateway
      namespace: gateway-system
      sectionName: https
  hostnames:
    - kaneo.example.com

By default the chart creates one Gateway API rule:

  1. / goes to the Kaneo service
    If you need custom matching or multiple backend references, override gateway.rules directly. Each backendRefs entry follows the same pattern as ingress and uses the chart-specific service field (kaneo), which is expanded to the release-specific Service name.

Security

For production deployments, consider the following security recommendations:

  1. Use secure AUTH_SECRET and PostgreSQL passwords, preferably stored in Kubernetes Secrets
  2. Enable TLS for ingress or Gateway API
  3. Enable and set resource limits to prevent resource exhaustion
  4. Use a dedicated storage class for the PostgreSQL database
  5. Consider using a network policy to restrict traffic between components
  6. Regularly update PostgreSQL and application images

Pod Security Context

On clusters with Pod Security Admission enforcement, set the following to satisfy the restricted policy:

yaml
podSecurityContext:
  runAsNonRoot: true
  seccompProfile:
    type: RuntimeDefault

kaneo:
  securityContext:
    allowPrivilegeEscalation: false
    capabilities:
      drop: ["ALL"]

OpenShift and read-only containers

Kaneo supports OpenShift's assigned UID. The chart mounts an emptyDir at /var/lib/kaneo for generated frontend assets, nginx state, and temporary files. These files are recreated at startup; application data remains in PostgreSQL and the configured object storage.

Leave runAsUser, runAsGroup, and fsGroup unset on OpenShift so its security policy can assign permitted IDs. With an external PostgreSQL database, the Kaneo container can use:

yaml
podSecurityContext:
  runAsNonRoot: true
  seccompProfile:
    type: RuntimeDefault

kaneo:
  securityContext:
    readOnlyRootFilesystem: true
    allowPrivilegeEscalation: false
    capabilities:
      drop: ["ALL"]

The bundled PostgreSQL image has its own platform requirements; use an OpenShift-compatible database deployment or an external database on OpenShift.

When running the combined or standalone web image outside Helm with a read-only root filesystem, mount writable storage at /var/lib/kaneo. For example:

bash
docker run --read-only \
  --tmpfs /var/lib/kaneo:rw,mode=0770,uid=1001,gid=0 \
  --env-file .env -p 5173:5173 ghcr.io/usekaneo/kaneo:latest

For a bind mount, grant write access to the container's UID or a group it belongs to. The image's default user is UID 1001 with GID 0; arbitrary UIDs can write its runtime directory through GID 0 when the root filesystem is writable.

nginx serves the web app from /var/lib/kaneo/html, where startup writes the runtime configuration, and reads server configuration copied from /etc/nginx/conf.d into /var/lib/kaneo/nginx/conf.d. If you mount your own server configuration, set root /var/lib/kaneo/html;. The previous /usr/share/nginx/html still holds the unconfigured build, so a config that serves it breaks API calls and sign-in; startup logs a warning when a config still uses that path. Files you mount into /usr/share/nginx/html, such as a custom favicon, are still copied into /var/lib/kaneo/html and served, and startup never writes to them. Templates in /etc/nginx/templates still render at startup, and default.conf.template replaces the bundled server configuration.

Registration Control

By default, user registration is enabled. To disable new user registration:

yaml
kaneo:
  env:
    disableRegistration: true

This will prevent new users from registering while still allowing existing users to log in. The registration option will be hidden from the login page.

Authentication secret storage

The chart stores kaneo.env.authSecret in a Kubernetes Secret and the application Deployment references its auth-secret key. Upgrades preserve the supplied value; the chart does not generate a new signing secret. For production, prefer kaneo.env.existingSecret so secret material is not supplied in Helm values or retained in Helm release metadata. Limit access to Secrets and Helm release records.

When upgrading from a chart that placed AUTH_SECRET directly in the Deployment, consider the old Deployment, ReplicaSets, exported manifests and deployment logs exposed to readers of those resources. If those readers were not authorized to possess the signing secret, rotate it and invalidate affected authentication artifacts as part of the upgrade. Moving the value into a Secret does not revoke earlier copies.


Overview (`packages/cli/README.md`)

Kaneo CLI

The official command-line client for Kaneo, the open source project manager. It is maintained in the usekaneo/kaneo monorepo and ships as a single kaneo binary for macOS, Linux and Windows.

bash
curl -fsSL https://kaneo.app/cli/install.sh | sh
kaneo login
kaneo task list

It works with Kaneo Cloud and with self-hosted instances, and prints styled output in a terminal and plain JSON everywhere else.

Install

macOS and Linux:

bash
curl -fsSL https://kaneo.app/cli/install.sh | sh

Windows (PowerShell):

powershell
irm https://kaneo.app/cli/install.ps1 | iex

With Homebrew, or with npm and Node.js 20 or newer:

bash
brew install usekaneo/tap/kaneo
npm install @kaneo/cli

The script picks the build for your system from the cli-v<version> GitHub release, verifies it against the release's SHA256SUMS, and installs it to ~/.local/bin/kaneo (Windows: %LOCALAPPDATA%\Programs\kaneo\kaneo.exe, added to your user PATH). Both scripts are plain text, so you can read install.sh and install.ps1 before running them.

Variable What it does
KANEO_VERSION Install this version, such as 0.1.0 or cli-v0.1.0, instead of the latest
KANEO_INSTALL_DIR Install into this directory
KANEO_DOWNLOAD_URL Download from a mirror that has the GitHub release layout (/cli-v<version>/<file>) instead of GitHub. Requires KANEO_VERSION
bash
curl -fsSL https://kaneo.app/cli/install.sh | KANEO_VERSION=0.1.0 sh
curl -fsSL https://kaneo.app/cli/install.sh | KANEO_INSTALL_DIR="$HOME/bin" sh
curl -fsSL https://kaneo.app/cli/install.sh | KANEO_DOWNLOAD_URL=https://mirror.example.com/kaneo KANEO_VERSION=0.1.0 sh
powershell
$env:KANEO_VERSION = "0.1.0"; irm https://kaneo.app/cli/install.ps1 | iex

To update, run the install command again, brew upgrade kaneo for Homebrew, or npm install @kaneo/cli@latest for npm. To uninstall, delete the binary, and ~/.config/kaneo too if you want to remove your logins.

Prefer the script to downloading a binary from the release page with a browser: the macOS builds are signed ad hoc but not notarized, so Gatekeeper can block browser downloads. On Alpine and other musl distributions, install the C++ runtime first with apk add libstdc++. See the install guide for mirrors, offline installs and supported systems.

Sign in

kaneo login uses device authorization: it shows a one-time code, copies it to your clipboard, and opens the approval page in your browser. Approve it there and the CLI stores a token for that server.

bash
kaneo login                                   # Kaneo Cloud
kaneo login --api-url https://kaneo.example.com
kaneo whoami
kaneo logout

For CI and scripts, use an API key instead (create one in Settings, then Account, then API Keys):

bash
export KANEO_API_KEY=...
kaneo task list -w <workspace-id> -p KAN

Credentials are resolved in this order: --token, then KANEO_API_KEY, then the stored login. A stored login is only sent to the server it was created for.

Commands

Command What it does
kaneo Home screen: your account, workspace, open tasks and the main commands
kaneo login, logout, whoami Sign in, sign out, show the current account and server
kaneo profile list, use, rename, remove Manage stored logins for several servers or accounts
kaneo task list, mine, view, open List a project's tasks or your own, show one, open it in the browser
kaneo task create, edit, duplicate, delete Create, change, copy and delete tasks
kaneo task status, move, assign, field Change a task's column, project, assignee or custom fields
kaneo task bulk Apply one change to many tasks at once
kaneo task attach, images Upload files to a task, show the images in its description
kaneo task relation, link, activity Subtasks and blockers, links, and the task's history
kaneo task export, import Export a project's tasks as JSON and create tasks from such a file
kaneo comment list, add, edit, delete Read and manage task comments
kaneo label list, create, edit, delete Manage workspace labels
kaneo time start, stop, status, log, list, edit Track time with a timer or log it afterwards
kaneo project list, view, create, edit, archive, unarchive, move, delete Manage projects
kaneo column list, create, edit, move, delete Manage a project's columns
kaneo field list, create, delete Manage a project's custom fields
kaneo workflow list, set, delete Move tasks when GitHub, GitLab or Gitea events arrive
kaneo workspace list, use, create, edit, leave, delete Choose and manage workspaces
kaneo member list, invite, role, remove Manage workspace members
kaneo invitation list, pending, accept, decline, cancel Manage invitations you sent or received
kaneo notification list, read Read your notifications (also kaneo inbox)
kaneo search Search tasks, projects and comments
kaneo link, unlink Save or remove the workspace and project for a repository
kaneo context Show the settings in use and where each came from
kaneo doctor Check the server, your login and API compatibility
kaneo api Send an authenticated request to any API endpoint, like gh api

The command reference lists every argument and flag. Run any command with --help for the same information, and kaneo --completions zsh (or bash, fish, sh) to print a shell completion script.

Tasks are addressed by their ticket ID, such as KAN-123, or by id. When a required workspace, project or column is missing and you are in a terminal, the CLI offers a searchable picker. In scripts it fails with an error that says which flag to pass.

Commands that delete or remove data ask for confirmation in a terminal and need --yes in scripts. Comments and descriptions can be read from a file with -F <file>, or from stdin with -.

Output

Human mode is the default in a terminal. JSON mode is used with --json or --jq, with KANEO_JSON=true, or whenever stdout is not a terminal:

  • A successful command prints exactly one JSON value on stdout.
  • A failed command prints {"error": "message"} on stdout and exits with code 1.
  • JSON mode never prints colors, spinners, images or extra lines.

--jq filters the JSON with a built-in jq engine, so jq does not need to be installed. Strings print without quotes, one result per line:

bash
kaneo task list -p KAN --jq '.[] | select(.priority == "urgent") | .ticketId'

Use --human to force styled output. Styled output respects NO_COLOR, FORCE_COLOR and TERM=dumb, and falls back to ASCII when the terminal cannot show Unicode.

Images in task descriptions (kaneo task images), uploads (kaneo task attach) and your avatar (kaneo whoami) are shown inline in Kitty, Ghostty, iTerm2 and WezTerm. Other terminals with 256 colors and Unicode get a preview drawn with colored blocks, and the rest get links. Set KANEO_IMAGES=kitty|iterm|blocks|off to override this. Your token is only sent with requests for files on your Kaneo server.

Agent skill

The kaneo-cli skill teaches coding agents such as Claude Code, Codex and Cursor to use the CLI: check the login and workspace, read JSON output, address tasks by ticket ID, and confirm before deleting anything.

bash
npx skills add usekaneo/kaneo --skill kaneo-cli

Configuration

Setting Flag Environment
Server --api-url KANEO_API_URL (default https://cloud.kaneo.app)
Token --token KANEO_API_KEY
Workspace -w, --workspace KANEO_WORKSPACE
Project -p, --project KANEO_PROJECT
Stored login --profile KANEO_PROFILE
JSON output --json KANEO_JSON=true
Inline images KANEO_IMAGES (kitty, iterm, blocks or off)
Web app address for links KANEO_WEB_URL (detected at login)
Config file KANEO_CONFIG

Logins live in ~/.config/kaneo/config.json (or $XDG_CONFIG_HOME/kaneo/config.json), readable only by you. The running timer is tracked in timer.json next to it. If that file was written by a different kaneo CLI, this one leaves it untouched and asks you to move it or set KANEO_CONFIG.

kaneo link saves the workspace and project for a repository in a .kaneo.json at its root, and kaneo unlink removes them:

json
{ "workspace": "your-workspace-id", "project": "KAN" }

The workspace comes from -w, then KANEO_WORKSPACE, then .kaneo.json, then the default from kaneo workspace use. The project comes from -p, then KANEO_PROJECT, then .kaneo.json. Run kaneo context to see which values are in use and where each came from.

Develop

bash
pnpm install
pnpm --filter @kaneo/cli build     # bundles to dist/kaneo.mjs
node packages/cli/dist/kaneo.mjs --help
pnpm --filter @kaneo/cli test
pnpm --filter @kaneo/cli typecheck

The CLI is written with Effect 4 (effect/cli, effect/http, effect/schema) and bundled into one file with no runtime dependencies, so kaneo --version starts in about 50 ms. Commands are Effect.fn functions, failures are tagged errors mapped in one place (src/errors/describe.ts), and the API client, config store and terminal are services that tests replace with layers.

Releasing

Bump version in package.json and merge to main. The Release CLI workflow (.github/workflows/publish-cli.yml) tests and bundles the CLI, compiles standalone binaries with Bun for macOS, Linux (glibc and musl) and Windows, and creates the cli-v<version> GitHub release with the binaries and a SHA256SUMS file. It skips versions that already have a release, and you can also run it by hand from the Actions tab.

After a stable release, the workflow also points the kaneo formula in usekaneo/homebrew-tap at the new binaries. It writes the formula with scripts/homebrew-formula.sh and pushes with the HOMEBREW_TAP_TOKEN secret, a fine-grained token that can write to that repository only.

While the CLI_NPM_PUBLISH repository variable is true, the same workflow also publishes @kaneo/cli to npm with provenance through trusted publishing, skipping versions that are already published.


Overview (`packages/mcp/README.md`)

Kaneo MCP server

@kaneo/mcp is the official MCP (Model Context Protocol) server for Kaneo, the open source project management platform. It is maintained in the usekaneo/kaneo monorepo and published to npm by the Kaneo team.

It runs over stdio, signs in with Kaneo's device flow, and then calls the Kaneo API with a bearer token. The package lives in packages/mcp in this monorepo and exposes the kaneo-mcp CLI.

Tip: Every Kaneo instance also ships a built-in HTTP MCP endpoint at /api/mcp. If your MCP client supports Streamable HTTP transport (e.g. Claude Code), you can connect directly without this package. See the MCP docs for details.

Prerequisites

  • Node.js 24+
  • A running Kaneo API (for example http://localhost:1337) and web app (for device approval UI).

Kaneo allows kaneo-cli and kaneo-mcp by default, so you usually do not need extra server configuration.

If you want to run this server with a different client ID, allow it on the Kaneo server:

bash
DEVICE_AUTH_CLIENT_IDS=kaneo-cli,kaneo-mcp,your-client-id

Environment

Variable Description
KANEO_API_URL Kaneo API origin (default http://localhost:1337). Do not include /api.
KANEO_MCP_CLIENT_ID Device-flow client id (default kaneo-mcp). Must match DEVICE_AUTH_CLIENT_IDS on the server.
KANEO_API_KEY Optional. A Kaneo API key (create one under Settings โ†’ Account โ†’ Developer). When set, the server authenticates with it as a Bearer token and skips the interactive device flow. Use this for headless/Docker setups.

Install

Recommended (no global install): run the interactive installer with npx:

bash
npx @kaneo/mcp

npm downloads the package, then an interactive menu (arrow keys + Enter) asks where to register the server (Cursor user-wide, Cursor project, Claude Desktop, or a custom JSON path). It then merges a mcpServers entry that points at this packageโ€™s dist/index.js with your current Node binary.

In a normal terminal, npx @kaneo/mcp and kaneo-mcp with no subcommand both start the installer. When the process is not attached to a TTY (for example when Cursor launches the MCP server with a pipe), the same entry runs the stdio MCP server instead.

To run the server manually from a shell (for example to debug stdio), use:

bash
npx @kaneo/mcp serve

If you prefer a global install:

bash
npm install @kaneo/mcp
kaneo-mcp

(kaneo-mcp install is the same installer with an explicit subcommand.)

Non-interactive example (Cursor user config, skip overwrite prompts):

bash
kaneo-mcp install --target cursor-user -y

Point at a self-hosted API when generating the config:

bash
kaneo-mcp install --target cursor-user -y --api-url https://kaneo.example.com

See all options:

bash
kaneo-mcp install --help

If you are currently inside the local packages/mcp package directory, npm may resolve the local workspace package instead of the published one and fail to expose the bin. In that case, either run npx from outside packages/mcp, or use a local build:

bash
node dist/index.js

The published package includes dist/. prepublishOnly runs the build before publish.

Develop from source

From the repo root:

bash
pnpm install
pnpm --filter @kaneo/mcp run build
pnpm --filter @kaneo/mcp run start
pnpm --filter @kaneo/mcp run test

Or run it from the package directory:

bash
pnpm -C packages/mcp run build

The CLI entry points to ./dist/index.js. Use npx @kaneo/mcp or kaneo-mcp after a global install so your IDE config points at the resolved path.

Authentication

On the first tool call that needs Kaneo, the server:

  1. Requests a device code from POST /api/auth/device/code
  2. Prints the verification URL and user code to stderr
  3. Tries to open the browser
  4. Polls POST /api/auth/device/token until approved
  5. Stores the access token at ~/.config/kaneo-mcp/credentials.json with mode 0600

Non-interactive (API key)

For headless or sandboxed environments where opening a browser is impractical, set KANEO_API_KEY to a key created under Settings โ†’ Account โ†’ Developer. The server sends it as a Bearer token on every request and skips the device flow entirely, so no token is cached to disk.

Tools

  • Session: whoami, list_workspaces, list_workspace_members, list_notifications
  • Search: search
  • Projects: list_projects, get_project, create_project, update_project, list_project_columns
  • Tasks: list_tasks, get_task, get_task_by_ticket_id, create_task, update_task, delete_task, move_task, update_task_status, update_task_assignee, update_task_due_date, list_task_activity
  • Custom fields: list_project_custom_fields, get_task_custom_field_values, set_task_custom_field_value
  • Time entries: list_task_time_entries, get_time_entry, create_time_entry, update_time_entry
  • Comments: list_task_comments, create_task_comment, update_task_comment, delete_task_comment
  • Labels: list_workspace_labels, create_label, attach_label_to_task, detach_label_from_task, delete_label
  • Task relations: create_task_relation, get_task_relations, delete_task_relation

Call list_project_columns before setting a status: the column slugs it
returns are the values create_task and update_task_status accept.
Call get_task_by_ticket_id with a key such as KAN-12 to retrieve one task
without listing a project. If the same key identifies multiple accessible tasks,
pass workspaceId or projectId to select one.
list_workspace_members resolves the user IDs the assignee tools expect.
Time entries have no delete endpoint on the API, so there is no
delete_time_entry tool.
Call list_project_custom_fields for the field IDs and options
set_task_custom_field_value expects. Pass multiselect values as an array of
options, dates as YYYY-MM-DD, and null to clear a value.

Releasing

Bump version in packages/mcp/package.json and merge to main. The publish workflow runs the package tests, publishes the new version to npm, and creates a mcp-v<version> GitHub release. Nothing is published while the version stays the same, so tool changes reach npm only once the version is bumped.

Publishing a GitHub release manually also works: tag it mcp-v<version> with the tag version matching packages/mcp/package.json on the tagged commit.


Overview (`packages/planka-import/README.md`)

@kaneo/planka-import

Migrate your PLANKA boards into Kaneo.

The CLI reads boards directly from PLANKAโ€™s REST API and creates them through Kaneoโ€™s public API. It does not modify PLANKA or use an intermediary migration service.

Usage

bash
npx @kaneo/planka-import --planka-url https://planka.example.com --dry-run

A dry run reads PLANKA only and prints exactly what would be created. When the
plan looks right, set KANEO_API_KEY in your environment and drop --dry-run:

bash
npx @kaneo/planka-import \
  --planka-url https://planka.example.com \
  --kaneo-url https://cloud.kaneo.app \
  --workspace ws_123

You'll be prompted for your PLANKA login and for which boards to migrate.

Create a Kaneo API key under Settings โ†’ Account โ†’ API Keys. Self-hosting? Point
--kaneo-url at your own instance.

What carries over

PLANKA Kaneo
Board Project
List Column (Closed lists become the final column)
Card Task
Card description Task description
Checklists (task lists) Markdown checkboxes appended to the description
Checklist items linked to another card Task relations (subtask)
Labels Labels, with their original colors
Card members Assignee, matched by email address
Due date Due date
Comments Comments, with the original author recorded separately and the original date added to the text

One Kaneo project is created per PLANKA board, since the board is what holds
lists and cards. If a PLANKA project has several boards, each Kaneo project is
named Project - Board.

What doesn't

  • Attachments. Counted and reported, but not transferred.
  • Archive and trash lists. Skipped by design; only active and closed lists
    are migrated.
  • Comment authorship. The comment is created by the API key's owner, but the
    original PLANKA author is recorded and displayed alongside it. Requires Kaneo
    2.17.6 or newer.
  • Priorities. The importer sets every task to
    no-priority.
  • Custom fields, stopwatches, and card subscriptions. The importer does not transfer these. Kaneo supports custom fields and time entries, but you need to recreate that information yourself.

Run the import as a PLANKA admin. PLANKA hides other users' email addresses
from non-admin accounts, and assignees are matched by email, so a non-admin
import leaves every task unassigned.

Assignees also only match when the person already exists in the target Kaneo
workspace with the same email address. Invite your team first, then import, and
assignments come across on the first run.

Options

Flag Description
--planka-url PLANKA instance URL (required)
--planka-user Email or username (prompted if omitted)
--planka-password Password (or set PLANKA_PASSWORD)
--planka-token <token> Use an existing access token instead of logging in
--planka-api-key <key> Use a PLANKA API key (or PLANKA_API_KEY). Works for SSO-only accounts
--kaneo-url Kaneo instance URL (default https://cloud.kaneo.app)
--kaneo-api-key <key> Kaneo API key (or set KANEO_API_KEY)
--workspace Target workspace (prompted if omitted)
--project <name|id> Migrate only this PLANKA project (repeatable)
--all Migrate every board without prompting
--dry-run Report what would happen, write nothing
--skip-comments Don't migrate comments
--icon <name> Lucide icon for created projects (default Layout)
--report Write a JSON report
-y, --yes Skip the confirmation prompt

If your PLANKA account uses SSO or has two-factor authentication enabled, there
is no password to log in with. Create a PLANKA API key for the account and pass
--planka-api-key instead.

Re-running

The importer always creates new projects; it does not update ones it created
earlier. If an import goes wrong, delete the created Kaneo project and run it
again. Board-level failures are isolated, so one broken board won't stop the
rest, and the summary tells you which ones failed.

License

MIT


Overview (`scripts/ci/README.md`)

CI checks

ci.yml runs on PRs and main, and is reused by Nightly. It checks formatting,
translations, OpenAPI, types, unit/integration tests, site links, workflow syntax,
real S3 uploads, and all three Dockerfiles. The bundled image is loaded and run
against disposable PostgreSQL and MinIO services for browser, realtime and
upgrade tests. Runtime failures retain container logs, a browser screenshot and
a Playwright trace for seven days.

The disposable S3 fixture pins Bitnami's archived MinIO 2025.7.23 image by its
multi-platform digest because the upstream public registries no longer serve
the previous image. It runs the MinIO binary directly with temporary test data.

  • browser.mjs: real sign-in, project/task creation, status changes, another
    tab's realtime cache update, reload persistence and cross-workspace denial.
  • realtime.mjs: authenticated clients receive the same mutation with no Redis,
    and when connected to separate API instances sharing Redis.
  • upgrade.sh / upgrade.mjs: resolve the latest stable GitHub release to an
    image digest, seed it through public APIs, then replace it with the candidate
    image on the same database. Verify account credentials, owner membership,
    project/task fields, comments, private image bytes and subsequent writes.
  • require-ci.mjs: release publication requires the latest CI run for the exact
    main SHA to succeed. It accepts push and manual CI runs, waits up to 30 minutes,
    and rejects failed, cancelled and skipped runs. If no push run exists (for
    example a release commit containing [skip ci]), dispatch CI on main before
    dispatching Release. Dry runs don't wait or publish.

The separate PR title check uses the base branch's commitlint policy and reruns
when the title changes. Helm validation also executes the existing secret and
upgrade-rendering regression checks. Configure the new job checks as required in
GitHub if they should block merging; editing workflows alone does not change
branch protection.

Contribution eligibility is a separate workflow. It posts a status on the
head commit of every open PR. Maintainers, vouched accounts, and approved bots
pass without an issue; everyone else has to link an open
ready-for-contribution issue from this repo. The workflow always takes its
code and policy from main, never from the PR. Voucher management and the
branch-rule setup are covered in
the contribution guide. Run
its tests with node --test scripts/contribution-eligibility/*.test.mjs.

Local runtime checks

Use disposable services only. The HTTP helpers refuse non-loopback origins.
No root .env, production credentials or cloud storage are needed. You need
Docker, Node, pnpm, the API workspace dependencies and the existing browser
runner's dependencies (npm ci --ignore-scripts --prefix scripts/ui-review-bot).

After building a candidate image tagged kaneo:ci:

sh
docker compose --env-file /dev/null -p kaneo-ci -f scripts/ci/compose.yml up -d --wait postgres minio app
node scripts/ci/browser.mjs http://127.0.0.1:55173
node scripts/ci/realtime.mjs http://127.0.0.1:55173
KANEO_CI_REDIS_URL=redis://redis:6379 docker compose --env-file /dev/null -p kaneo-ci -f scripts/ci/compose.yml --profile realtime up -d --wait app second redis
node scripts/ci/realtime.mjs http://127.0.0.1:55173 http://127.0.0.1:55174
GITHUB_REPOSITORY=usekaneo/kaneo GITHUB_REPOSITORY_OWNER=usekaneo bash scripts/ci/upgrade.sh

Install Chromium once using
scripts/ui-review-bot/node_modules/.bin/playwright install chromium.
Stop only this stack and remove its disposable data afterwards:

sh
docker compose --env-file /dev/null -p kaneo-ci -f scripts/ci/compose.yml --profile realtime --profile upgrade down -v

The stack reserves loopback ports 55173โ€“55175, 59040, 59379 and 59432. It uses
fixed synthetic credentials and test databases; do not expose it to the network.

For a local worktree on a copy-on-write filesystem, reuse the pnpm store without
copying dependency data:

sh
pnpm install --offline --frozen-lockfile --ignore-scripts --package-import-method=clone

This preserves independent workspace links. The logical node_modules size
still includes shared blocks; it is not the additional physical disk cost.


Overview (`scripts/slop-labeler/README.md`)

Slop labeler

Adds slop when a PR adds or changes two distinct JS/TS comment blocks with
at least 20 words each and classifier scores of 99% or higher. Settings live in
policy.mjs. Maintainer PRs are skipped.

The bot leaves one comment telling human authors they can ignore the result.
Later scans update that comment and remove the label if it no longer applies.
Manually added labels stay. Failed scans leave everything untouched.

Local tests found human false positives even at this threshold. A 99% score
doesn't mean 99% accuracy.

PR code is fetched as text and parsed, never executed. The workflow starts
running once merged into the default branch.

To scan an existing PR, run the workflow manually with its PR number.

Run the tests:

sh
cd scripts/slop-labeler
npm ci --ignore-scripts
npm test

The classifier comes from kqr's article.
vendor/ preserves the upstream source at
9d3abed60e628951c7b0d2cd1e1c69eddb23eb97, the
published model, and its license.
Tests check the model and inference hashes. Recheck thresholds before replacing
the model.


Overview (`scripts/ui-review-bot/README.md`)

Peekareq

A maintainer comments /peekareq on an open Kaneo PR. The bot selects up to three UI scenarios, captures the PR in Chromium, frames the changed component, uploads three distinct previews, and posts this line followed by a description/image table:

This UI-screenshot was auto-made by a beta tool made by @tinsever

Below the screenshot table, Findings (Beta) and Run info are collapsed by default. Findings contain axe-core accessibility results and clearly labeled AI suggestions with practical fixes. Run info lists the models that returned responses, provider-reported AI cost, and elapsed time from planning through review (including job waits). Re-runs update the bot's existing comment.

GitHub setup

  • Install the GitHub App on usekaneo/kaneo with Contents: Read and write and Pull requests: Read and write, and Issues: Read-only. The installation owner must approve permission updates.
  • Set Actions variable APP_CLIENT_ID and secrets APP_PRIVATE_KEY (the complete PEM file) and OPENROUTER_API_KEY.
  • Put this directory and both ui-review.yml and ui-review-run.yml workflows on the default branch.
  • Deploy worker/ to Cloudflare (see below). Enable the App webhook at its /webhook URL, set the same signing secret, and subscribe only to Issue comment.

Comment exactly /peekareq in a PR's conversation, or use Actions โ†’ Peekareq โ†’ Run workflow. Current write, maintain, or admin access is required. Bots, comment edits, quoted commands, arguments, ordinary issues, and closed PRs do not start a run. Failed permission checks stop the request before model calls. Only authorized requests can cancel an earlier run for the same PR.

The final job authenticates as <app-slug>[bot]. PNGs are stored on a dedicated code/ui-screenshots branch and embedded using immutable URLs. Concurrent uploads preserve earlier images. Failed captures and reports for a changed or closed PR are not posted.

Local use

Requires Node 24, pnpm, git, and authenticated gh with contents and PR-comment write access. Workflows use Node 24.

sh
npm --prefix scripts/ui-review-bot ci --ignore-scripts
npm --prefix scripts/ui-review-bot run setup
npm --prefix scripts/ui-review-bot start -- 1719

A PR number or URL is required. Runs upload and comment automatically; add --no-post to keep results local. The default key file is ~/.env.openroutercopythis, containing API_KEY=โ€ฆ or OPENROUTER_API_KEY=โ€ฆ. It is parsed as data, never executed.

sh
npm --prefix scripts/ui-review-bot start -- 1719 --no-post
npm --prefix scripts/ui-review-bot start -- 1719 --capture-only --no-post
npm --prefix scripts/ui-review-bot test
node scripts/ui-review-bot/publish.mjs /absolute/path/to/report.json

Use --model provider/model or --key-file /path/to/key.env to override defaults. Local App authentication uses GH_TOKEN and UI_REVIEW_BOT_LOGIN=<app-slug>[bot]; otherwise gh supplies your user identity.

Default model: qwen/qwen3.8-flash, verified against OpenRouter's live vision-model catalog. If its provider is rate-limited or temporarily unavailable, the run switches to google/gemini-2.5-flash-lite; explicitly selected other models keep their selection. Temporary failures retry at most twice and all attempts count toward the five-request budget. The model receives the PR diff, relevant source, synthetic page text, and screenshots. Each run allows at most five calls, three scenarios, and six interactions per scenario. Reports record provider-reported usage and cost; there is no fixed dollar cap.

Local reports, before/after PNGs, and pixel differences are saved in .cache/ui-review-bot/runs//. Ctrl+C cancels capture and closes previews.

Coverage and isolation

The current fixture adapter supports account settings, a synthetic workspace/project/task, stateful time-entry Start/Stop controls, and custom-field values. Multiselect changes use fixture-backed scenarios for project configuration, the option picker, and multiple selected values. Each scenario identifies the component to frame and controls that must be visible; portalled menus are included in the frame. Missing or clipped controls and identical previews prevent publication. Defined fixture scenarios use their short, specific names as captions. Marketing-site and docs-only changes stop during planning because the preview currently runs apps/web only. Each scenario gets its own fixture state. The capture-only preset is specific to PR #1719. Other features may require fixtures; unknown API reads produce incomplete captures. Full before/after captures use a 1440ร—1000 desktop viewport; GitHub receives bounded close-up previews captured directly in Chromium. Captions are limited to 70 characters. Captures use light theme, English, UTC, and disabled animations. The captured PR areas are audited with axe-core 4.13.0 using WCAG 2 A/AA, 2.1 A/AA, 2.2 AA, and best-practice rules. Confirmed violations and checks needing manual review remain distinct. AI also reviews visible clipping, overlap, readability, and interaction feedback. These checks do not establish full accessibility compliance or test real backend behavior.

Before images use the PR's merge base. Different routes or interaction states are labeled in the local report rather than treated as regression scores. Each revision uses its own frozen lockfile with install scripts disabled. Browser traffic is restricted to the local preview and synthetic API responses.

The Worker verifies GitHub signatures and current maintainer access before sending repository_dispatch. Ordinary comments never create Actions entries. Actions rechecks the original comment and author before planning. Authorization, planning, capture, and review/posting use separate GitHub-hosted runners. Only capture executes PR code, without provider secrets or a write token. Trusted jobs check out the workflow revision. Review accepts bounded capture JSON and PNGs, preserves the trusted plan's PR identity, rejects malformed images, and recalculates differences. App credentials are provided only after review. The default Actions token is read-only.

Local capture executes PR code as your OS user; it is not an OS sandbox. Use GitHub-hosted capture for untrusted PRs. Credentials are not passed into preview environments or written to reports. PR previews, short captions, bounded findings, and run statistics are published; intermediate workflow artifacts expire after one day.

Cloudflare webhook

The Worker is peekareq-webhook; endpoint: https://peekareq-webhook.marmeladenjunge.workers.dev/webhook. It uses the Workers Free-compatible request path and a small D1 table to deduplicate command comment IDs. The optional code-review relay also uses this Worker; browser captures continue to run in Actions. Its installation token is restricted to kaneo with Contents write and Pull requests read; repository dispatch uses the existing Contents permission.

sh
npx [email protected] d1 execute peekareq-commands --remote --file scripts/ui-review-bot/worker/schema.sql --config scripts/ui-review-bot/worker/wrangler.jsonc
npx [email protected] secret put APP_PRIVATE_KEY --config scripts/ui-review-bot/worker/wrangler.jsonc
npx [email protected] secret put WEBHOOK_SECRET --config scripts/ui-review-bot/worker/wrangler.jsonc
npx [email protected] deploy --config scripts/ui-review-bot/worker/wrangler.jsonc

Store the App key as PKCS#8 PEM (convert a downloaded PKCS#1 key with openssl pkcs8 -topk8 -nocrypt). Keep secrets out of source control. For a new account, create a D1 database and update its ID in wrangler.jsonc. The Worker checks the configured installation ID and repository before using App credentials. If dispatch times out after claiming a command, it deliberately does not replay it: check Actions first, then post a fresh command if needed.

Code review (Beta)

/peekareview requests a source-based review of an open PR. It uses the same live maintainer authorization and webhook filtering as /peekareq. Ordinary comments do not start either workflow. Re-runs update one Peekareq comment containing collapsed Findings (Beta) and Run info, including model, new AI cost, duration, and reviewed revision.

The code reviewer uses deepseek/deepseek-v4-flash through OpenRouter. It reads immutable base/head source without checking out or executing PR code. It retrieves callers, guards, constraints, and related tests; a second pass challenges each candidate, then a base-only pass checks that the behavior is newly introduced. Findings need validated source citations. Missing evidence, malformed responses, timeouts, and omitted files produce explicitly incomplete results. An empty review is not an approval.

Deployment requires code-review.yml, the updated Worker/schema, Worker secrets OPENROUTER_API_KEY and REVIEW_PROXY_TOKEN, Actions secret PEEKAREVIEW_PROXY_TOKEN with the same 64-character hexadecimal token, and Actions variable PEEKAREVIEW_MODEL_URL pointing to the Worker's /review-model endpoint. The provider key stays in the Worker for code reviews. The existing screenshot workflow keeps its separate provider-key configuration.

The relay requires an initialized review_budget row with id = 1, ceiling = 850000, and initial_spend equal to the already committed trial spending in integer microdollars. It refuses inference without initialization. The $0.85 trial ceiling includes prior development spend and a 15% allowance; it never resets automatically. Requests reserve maximum cost atomically before inference, and unknown charges remain reserved. Successful identical requests are cached; unresolved requests are not automatically retried within a run. An explicitly requested new run can try again while retaining the earlier reservation. A local review also has a $0.10 limit and a 12-minute deadline.

For a private local review:

sh
node scripts/ui-review-bot/code-review/cli.mjs 1735
node scripts/ui-review-bot/code-review/cli.mjs --base BASE_SHA --head HEAD_SHA
node scripts/ui-review-bot/code-review/cli.mjs --budget

Reports are saved under .cache/code-review/; the local CLI does not post to GitHub. The Actions publisher rechecks the original command, current maintainer access, PR head, and base immediately before posting.


Overview (`tests/api-integration/README.md`)

API integration tests

These tests boot the Hono app with a real PostgreSQL database (see setup.ts for env defaults).

  • Run: pnpm test:integration from the repo root (requires Postgres and DATABASE_URL).
  • CI: .github/workflows/ci.yml starts Postgres and runs the same command.

Coverage is intentionally incremental: add new files under this directory for additional routes or behaviors, following existing helpers (helpers/fixtures.ts, helpers/database.ts, helpers/auth.ts).


Overview (`tests/e2e/README.md`)

Browser smoke tests

These Playwright tests run against a disposable, bundled Kaneo instance built
from the current checkout. They exercise real authentication, workspace creation,
persistence after a reload, and authentication after clearing the session cookie.
They also create projects and tasks, persist task edits, and verify that another
workspace member receives edits through a real WebSocket without reloading.
PostgreSQL uses temporary storage; no existing database or root .env is used.

Local run

Requires Node 24, pnpm 10.32.1, and Docker Compose v2.

sh
pnpm --dir tests/e2e install --frozen-lockfile --ignore-scripts
pnpm --dir tests/e2e exec playwright install chromium
docker compose -f tests/e2e/compose.yml up --build --wait --wait-timeout 180
pnpm test:e2e
docker compose -f tests/e2e/compose.yml down --volumes

The test instance binds to http://localhost:18173. Tests create unique synthetic
users and workspaces. Stop the Compose stack after testing, including failed runs;
its temporary database is discarded. The stack's project name is
kaneo-browserstack-e2e; do not run two copies simultaneously on the same machine.
Reports, failure screenshots, and local Playwright traces are stored in .cache/e2e/.
BrowserStack disables native Playwright tracing; use its session recordings for cloud runs.

BrowserStack Automate

Set BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY in your shell's environment
using the credentials from your BrowserStack account. Keep them out of source
control and command-line arguments. Start the Compose stack with BrowserStack's
local hostname and the HTTPS override so the app, API, cookies, and WebSockets
share the same secure origin. This also requires OpenSSL:

sh
export KANEO_E2E_HOST=bs-local.com
export KANEO_E2E_TLS=true
export COMPOSE_FILE=tests/e2e/compose.yml:tests/e2e/compose.tls.yml
mkdir -p .cache/e2e-tls
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
  -keyout .cache/e2e-tls/key.pem -out .cache/e2e-tls/cert.pem \
  -subj '/CN=bs-local.com' -addext 'subjectAltName=DNS:bs-local.com'
docker compose up --build --wait --wait-timeout 180
pnpm test:browserstack
docker compose down --volumes
unset KANEO_E2E_HOST KANEO_E2E_TLS COMPOSE_FILE

BrowserStack rewrites localhost to bs-local.com for WebKit. Configuring that
hostname explicitly avoids mixing the page's origin with a localhost API URL.
The cloud endpoint is https://bs-local.com:18174. The test-only nginx gateway
uses a freshly generated, one-day self-signed certificate; the cloud test contexts
accept it. Certificate files stay outside uploaded reports. HTTPS avoids the HTTP
400 WebSocket handshake failures observed with WebKit through the Local tunnel.
Local Chromium PR runs continue to exercise the plain HTTP deployment.

The realtime test waits for the application's own keepalive before editing, then
asserts both the incoming task event and the updated board without reloading.

The SDK starts and stops BrowserStack Local to connect remote browsers to the
disposable instance. tests/e2e/browserstack.yml runs Chrome on Windows 11 and Playwright
WebKit on macOS Sequoia, with one session per platform (two concurrent sessions).
WebKit is an engine compatibility check; this configuration does not test Safari
on a real iPhone. Remove a platform if your plan allows only one parallel session.
Playwright is pinned to 1.62.1, from BrowserStack's supported 1.62 version family;
check their supported versions before upgrading it.

This integration uses Automate only. Test Reporting & Analytics is disabled;
Percy and other BrowserStack products are not configured. Session results and
videos appear in the Automate dashboard under Kaneo.

GitHub Actions

  1. Add repository Actions secrets BROWSERSTACK_USERNAME and
    BROWSERSTACK_ACCESS_KEY.
  2. Merge this integration into main.
  3. Every push to main now runs BrowserStack automatically. To rerun it,
    open Actions โ†’ BrowserStack โ†’ Run workflow, selecting main.

Browser tests runs the same suite in Chromium for every pull request, including
forks, without cloud credentials. BrowserStack runs Chrome on Windows and
WebKit on macOS after a push to main, or a manual dispatch on main. Cloud runs
are serialized to stay within the two-session limit; obsolete PR runs are cancelled.

Both workflows build the checked-out revision, use a disposable database, upload
reports for seven days, and tear down the stack even after failure. BrowserStack
credentials are passed only to its credential check and cloud test step. No
production instance is contacted. Tests use synthetic accounts; invitation setup
uses the normal API without sending email because SMTP is not configured.

Add the Chromium smoke tests check to branch protection when it has proven
stable. The cloud check runs after merge and does not currently gate releases.

References:


Overview (`tests/storage-integration/README.md`)

Local storage contract tests

These tests use a disposable S3-compatible service on 127.0.0.1 with the fixed, non-production credentials local-test-access / local-test-secret-only. They refuse remote endpoints, create a unique test bucket and delete only that bucket and its test objects afterwards. Never point them at an existing storage service.

Start a local MinIO test container bound to loopback with those root credentials:

sh
docker run --rm -d --name kaneo-storage-contract-test \
  -p 127.0.0.1:59039:9000 \
  -e MINIO_ROOT_USER=local-test-access \
  -e MINIO_ROOT_PASSWORD=local-test-secret-only \
  -e MINIO_BROWSER=off -e MINIO_UPDATE=off \
  --entrypoint /opt/bitnami/minio/bin/minio \
  bitnamilegacy/minio@sha256:8935e75fa5d11295c17171e4aa49efe390a1193cd7f12e4d21b92af9ffef09d7 \
  server /tmp/minio --address :9000

Once the service is ready, run:

sh
KANEO_STORAGE_TEST_ENDPOINT=http://127.0.0.1:59039 pnpm --filter @kaneo/api exec vitest run --config vitest.storage.config.ts
docker stop kaneo-storage-contract-test

The suite checks signed size/content-type enforcement, chunked-transfer rejection, actual object metadata, missing objects and oversized-object cleanup. The regular API integration suite separately covers endpoint authorization and database writes with mocked storage calls. No real cloud account or email provider is required.


Overview (`apps/docs/images/product/README.md`)

Documentation screenshots

These are captures of the real Kaneo frontend using fictional Studio North data. No live account or database is used.

Board, List, task, and Gantt images use the same fixture capture as apps/site/public/images/product/, in both light and dark themes. Their original viewport is 1728 ร— 1000 at 2ร— density. These eight images are checked-in captures; the settings capture script below does not regenerate them.

Backlog, members, labels, workflow, integrations, notifications, and security settings are captured at 1440 ร— 1000 with 2ร— density. From the repository root, run:

bash
node scripts/ui-review-bot/docs-screenshots.mjs

The script requires the UI review bot's Playwright dependencies and Chromium. It starts its own Vite server on port 5188, intercepts API calls with fixtures, blocks external requests, checks for browser errors and missing fixtures, and stops its server when finished.

Place screenshots in a Mintlify Frame, with block dark:hidden on the light image and hidden dark:block on the dark image. Include alt text and a caption. Review each changed capture before publishing.

The current documentation captures were refreshed against the local frontend on 2026-09-26. The fixture date controls sample task deadlines, not the capture date.

To capture only one view, set DOCS_SCREENSHOT_VIEW, for example DOCS_SCREENSHOT_VIEW=integrations node scripts/ui-review-bot/docs-screenshots.mjs.


Overview (`apps/site/public/cursors/README.md`)

Hand cursors from https://cursor.in/ by Sawyer Hollenshead.
Source: https://github.com/sawyerh/cursor.in

The SVG viewBoxes are cropped to remove transparent margins for the product tour.


Apps Docs Index (`apps/docs/index.mdx`)


title: "Kaneo docs"

Keep your projects, tasks, and conversations in one place. Start with a small project and add the setup your team needs as you go.

Create a workspace, invite a teammate, and finish your first task. Install Kaneo with Docker Compose, drim, or your hosting platform. Sync repositories, send project updates, and connect an AI assistant. Make your first request and explore the endpoint reference.

How Kaneo is organized

A workspace holds your team, projects, and shared labels. A project holds a body of work, such as a website launch. A task is one piece of that work, with its own status, assignee, description, and conversation.

If someone invited you, open their invitation link and sign in with the email address they invited. You can join their workspace without creating a new one.

Looking for something specific?

Screenshots throughout these guides show the real app with a fictional team, Studio North.


Apps Docs Cli Index (`apps/docs/cli/index.mdx`)


title: Kaneo CLI sidebarTitle: Overview

The official Kaneo CLI is a single kaneo binary for macOS, Linux and Windows. It works with Kaneo Cloud and self-hosted instances, shows styled output in a terminal, and prints plain JSON for scripts and agents.

Quick start

bash
curl -fsSL https://kaneo.app/cli/install.sh | sh
kaneo login
kaneo

On Windows, install it in PowerShell with irm https://kaneo.app/cli/install.ps1 | iex. Homebrew (brew install usekaneo/tap/kaneo) and npm (npm install @kaneo/cli, Node.js 20 or newer) work too. Install covers versions, mirrors and supported systems, and Sign in covers self-hosted servers and API keys.

Everyday commands

bash
kaneo workspace list
kaneo workspace use            # pick the default workspace
kaneo project list
kaneo task list -p KAN         # tasks grouped by column
kaneo task list -p KAN --mine --open
kaneo task mine                # your open tasks, soonest due first
kaneo task view KAN-12
kaneo task create "Fix login redirect" -p KAN --priority high --due tomorrow
kaneo task status KAN-12 done
kaneo task assign KAN-12 me
kaneo comment add KAN-12 "Shipped in v2.35"
kaneo time start KAN-12
kaneo time stop
kaneo task open KAN-12
kaneo search "login redirect"

Tasks are addressed by their ticket ID, such as KAN-12, or by their ID. If a ticket ID exists in more than one of your workspaces, pass -w to choose one.

When a workspace, project or column is missing and you are in a terminal, the CLI shows a searchable picker. In scripts it stops with an error that names the flag to pass.

Every command has --help, and the command reference lists every argument and flag. Print a shell completion script with kaneo --completions zsh (or bash, fish, sh).

Update

Run the install command again. It replaces kaneo with the latest release and keeps your logins and settings. See Install for pinned versions and mirrors.

Next steps

Pin a version, use a mirror, update and uninstall. Self-hosted servers, API keys for CI, and several logins. Environment variables, repository defaults and inline images. JSON output, jq filters, raw API calls and the agent skill. Every command with its arguments and flags. Connect AI assistants such as Claude and Cursor without a shell.

Apps Docs Core Index (`apps/docs/core/index.mdx`)


title: "Your first project"

You need a Kaneo account and access to an instance. Use Kaneo Cloud, join your team's instance, or host your own.

We'll use a website launch as an example. You can follow along with your own project name.

1. Create a workspace

After signing in, create a workspace for your team, for example Studio North. If you already belong to a workspace, use the workspace menu at the top of the sidebar to switch to it.

A workspace is the boundary for membership and permissions. Separate teams that should not share access belong in separate workspaces.

2. Add a project

Moving from Linear, Jira, Trello, Plane, or PLANKA? [Import your projects](/core/migrations) instead of starting from scratch.

Choose Add project in the sidebar and create Website launch. Open the project, then choose Tasks. Board and List are two views of the same tasks.

Start with the existing columns. You can change the workflow later.

3. Write the first task

Use the + button in a board column to add a task. Call it Build the new homepage, then open it.

Add a short description of what needs to be true when the task is finished. For example:

Publish the new homepage with approved copy, a working contact link, and a layout checked on mobile.

Use a checklist for the smaller steps. Set a due date if there is a real deadline, and add a label such as Design if it will help you find related work.

4. Invite a teammate

Open Members in the workspace sidebar and choose Invite member. Enter your teammate's email address, then copy the invitation link and share it with them. If your instance has email configured, Kaneo can also deliver the invitation by email.

Your teammate opens the link, signs in with the invited email address, and accepts. Return to the task and choose an assignee. If that control is unavailable, ask a workspace admin to check your role.

See Members and roles for pending invitations and access changes.

5. Move the work forward

Drag the task into In Progress when work starts. Add questions and updates in the task's comments so the context stays with the work.

When the work is finished, move it to the project's done column. If it needs another pass, move it back to an active column.

You now have a shared project with a task your team can follow from start to finish.

Explore Board, List, filters, and task details. Keep ideas in the backlog until they are ready.

Apps Docs Core Functional Index (`apps/docs/core/functional/index.mdx`)


title: "Using Kaneo"

New to Kaneo? Start with Your first project.

You want toโ€ฆ Read
Set up a team or another project Workspaces and projects
Create, assign, or find a task Board, List, and task details
Prepare work for later Backlog
See work over time Calendar and Gantt
Invite someone or change their access Members and roles
Change project columns Workflows
Organize related tasks Labels
Choose which updates you receive Notifications
Change the theme or task display Preferences
Update your profile or delete your account Your account

Apps Docs Core Installation Index (`apps/docs/core/installation/index.mdx`)


title: "Self-host Kaneo"

Kaneo runs as one application container plus PostgreSQL. You can add file storage and email when you need them. Redis is only needed when multiple API instances must share live updates.

Choose your setup

Setup Choose this whenโ€ฆ
Docker Compose You want to see and control the configuration. Start here for a local trial or an existing server.
drim You want a CLI to generate the deployment and set up Caddy for HTTPS.
Coolify You already deploy applications through Coolify.
Railway You want a hosted deployment without managing a server.
Helm You already operate a Kubernetes cluster.

For a small installation, plan for at least 2 GB of memory and 10 GB of free disk as a starting point. Leave additional space for database growth, uploaded files, and backups. Compose deployments need Docker and Docker Compose V2.

What runs where

mermaid
flowchart LR
  Browser[Your browser] -->|HTTPS| Proxy[Reverse proxy]
  Proxy --> Kaneo[Kaneo: web app and API]
  Kaneo --> DB[(PostgreSQL)]
  Browser -->|File uploads| Storage[(Optional S3 storage)]
  Kaneo -->|Read and validate files| Storage

PostgreSQL stores accounts, projects, tasks, comments, and upload records. File contents live in your configured object storage. Account avatars are stored in PostgreSQL.

Before inviting your team

  1. Put the instance behind HTTPS.
  2. Create the initial administrator account before opening registration to others.
  3. Configure email and access settings.
  4. Add Silo file storage if your team needs attachments.
  5. Make a backup and test a restore.

Then follow Your first project.


Apps Docs Core Integrations Index (`apps/docs/core/integrations/index.mdx`)


title: Integrations

Connect the tools your team already uses. Repository connections and shared notifications belong to a project. Personal notification channels belong to your account.

Repositories

Sync issues and comments, link pull requests, and automate task status. Connect your Gitea server for issue sync and workflow automation. Connect GitLab.com or your own instance to sync issues and track merge requests.

GitHub, Gitea, and GitLab have connection forms under Settings โ†’ Projects โ†’ your project โ†’ Integrations. Follow the provider guide to connect a repository and configure its webhook.

Limit repository sync by label

Enable Advanced settings in Account โ†’ Preferences or Workspace โ†’ General, then open a connected GitHub, Gitea, or GitLab integration. Workspace settings managers can edit its Sync scope. The saved rules apply to everyone in the project; turning advanced mode off only hides the editor.

  • Kaneo โ†’ repository selects workspace labels by stable ID. Match any selected label or require all of them. The preview shows matching tasks, new exports, and affected links before you apply the rule.
  • Repository โ†’ Kaneo selects exact repository label names for new tasks and imports. Existing linked tasks follow the Kaneo rule. Adding a qualifying repository label can admit an issue that was previously excluded.

When a linked task loses its qualifying labels, synchronization pauses in both directions. Both records and their link remain. Reapplying the label does not overwrite either side: choose Review and resume, compare the current title, description, and open/closed state, then keep the Kaneo or repository values. Historical comments are not replayed. Missing or deleted workspace labels keep the filter closed until you repair the rule.

Resuming leaves priority, detailed status, and custom labels unchanged. Later repository label events apply changes made after the comparison, so an unrelated label event does not restore stale values from the pause. Issue filters do not prevent branch or pull request linking and their configured status transitions.

Kaneo rereads the issue when you resume and rejects a changed comparison. Local edits remain available while the repository responds; a task, rule, label, or link change during a resume write keeps synchronization paused for another review. When adopting a different open/closed state, resume uses the project's configured issue close or reopen workflow target. Gitea's content version, when available, also protects the body write from concurrent edits. Provider APIs do not offer an atomic version check for every issue field, so an edit made after the final read can still conflict with a resume write. Avoid editing the repository issue while resuming it. A rejected provider write leaves synchronization paused for another review.

Kaneo checks the repository again after either resume choice, verifying the selected title, description, and open/closed state along with the original labels. A mismatch or failed verification pauses the link for another review, leaving priority and custom labels unchanged. Adopted values remain visible in the next comparison if verification fails.

An issue creation request already sent to a repository may finish after its task stops matching. Kaneo keeps the returned issue link paused and stops further updates. Local edits remain available while the repository responds.

If creating an issue succeeds but its initial state, labels, or linking comment fails, Export matching tasks retries the unfinished steps using the existing issue link.

Newly matching tasks without an issue link are exported in the background when rules or task labels change. Use Export matching tasks to retry tasks that still have no issue link after a failed export. An inactive integration does not export or resume tasks. Existing installations retain unrestricted sync until a rule is saved.

Team notifications

Send selected project events to a shared channel. Open Settings โ†’ Projects โ†’ your project โ†’ Integrations to connect these services.

Post task updates to a Slack channel through an incoming webhook. Send task activity to a Discord channel. Connect an incoming webhook on your Mattermost instance. Send updates through a bot to a chat, group, channel, or forum topic.

Personal notifications

Receive updates meant for you, such as assignments, mentions, and due-date reminders. Connect these under Settings โ†’ Account โ†’ Notifications, then choose the workspaces and projects that can send them.

Publish your notifications to an ntfy topic. Receive notifications through your Gotify server. Send updates to your account email. Self-hosted instances need SMTP. Forward your own notifications to an HTTP endpoint.

Notification preferences control the events and workspace rules for all four channels.

Automation and AI assistants

Send selected project events to your service as signed JSON requests. Let an MCP client work with your Kaneo tasks and projects. Create tasks, query projects, or build a connection to another service.

Looking for sign-in or file storage?

Sign-in providers are configured by the instance operator: GitHub, Google, Discord, or custom OAuth/OIDC.

For attachments, use Silo or another S3-compatible provider.


Apps Docs Core Migrations Index (`apps/docs/core/migrations/index.mdx`)


title: Import from other tools

Kaneo can copy projects from another tool into your workspace. You connect a source with a key, choose the projects you want, and Kaneo recreates them as new Kaneo projects. Kaneo only reads from the source; nothing there is changed. Imports work on Kaneo Cloud and on self-hosted instances.

Sources

Source Each of these becomes a Kaneo project Guide
Linear Team Import from Linear
Jira (Cloud or Data Center) Project Import from Jira
Trello Board Import from Trello
Plane (cloud or self-hosted) Project Import from Plane
PLANKA Board Import from PLANKA
Another Kaneo instance Project Import from another Kaneo instance

Start an import

Only workspace admins can import. Importing needs permission to manage workspace settings and to create projects, which owners and admins have by default.

Open **Settings โ†’ Workspace โ†’ Import**. You can also click **Import from another tool** on the projects page while the workspace has no projects, or **Import your projects** in the dialog for creating a new project. Choose where you are importing from and paste the key or token it needs. Each source guide explains where to find it. Select the projects to import, up to 50 at a time. Comments are included by default; turn them off if you don't need them. The import runs on the server, so you can leave the page and come back. A workspace runs one import at a time. **Recent imports** lists past runs with a report for each project (counts and warnings) and a link to the new project.

How imports work

These rules apply to every source.

Each import creates new projects

Every selected source project becomes a new Kaneo project. Kaneo never updates or merges into an existing project, so running the same import twice gives you two copies.

If a project fails to import, nothing of it is left behind. If an import finishes but the result is not what you want, delete the project and import it again.

Project keys and task numbers

The new project keeps the source's project key, for example WEB from Plane or ENG from Linear, when no other project in the workspace uses it. If the key is taken, Kaneo adds a suffix, for example WEB-2.

Tasks keep their numbers from the source, so WEB-12 stays WEB-12. A task without a number in the source, or with a number another task in the same import already has, gets the next number after the highest one. Tasks you create later continue from there.

Trello boards have no key, so Kaneo makes one from the board name. PLANKA boards have no key or task numbers either, so Kaneo makes a key from the new project's name and numbers the tasks from 1 in board order.

Invite your team first

Assignees are matched to workspace members by email. Anyone who isn't a member when you import gets their tasks imported unassigned. Invite your team before you start.

Some sources hide email addresses. Trello never shares other people's emails, Jira often hides them depending on each person's profile settings, and PLANKA hides them from accounts that are not admins. In those cases Kaneo matches by full name, ignoring case, and only when exactly one workspace member has that name.

Kaneo has one assignee per task. When an item has several assignees, only the first is kept, and the report says how many items had more than one.

Comments and notifications

Imported comments keep the original author's name and date, and are marked Imported from followed by the source name, for example Imported from Linear.

Imported tasks do not send notifications.

Your key is not stored

The key or token you paste is used for that one import and is not stored. Kaneo only reads from the source. To import again later, paste the key again. You can revoke it in the source once the import is done.

Self-hosted instances

The import runs on the Kaneo API server, not in your browser. The server must be able to reach the source.

By default, Kaneo blocks imports from addresses on a private network, for example a PLANKA container in the same Docker network or a Plane server on your LAN. To allow them, set KANEO_ALLOW_PRIVATE_WEBHOOK_DESTINATIONS=true on the API. This is the same switch that notification destinations (such as ntfy, Gotify, and webhooks) and the Gitea and GitLab integrations use, so only enable it on a trusted deployment. See environment variables.

The one-click Authorize on Trello button needs TRELLO_API_KEY on the API. See Set up Trello on a self-hosted instance.

If your source is on a private network and your Kaneo instance can't reach it (for example, you use Kaneo Cloud), the in-app importer can't connect. For PLANKA, use the command line importer, which runs on your own machine.


Apps Docs CONTRIBUTING (`apps/docs/CONTRIBUTING.md`)

Contribute to the docs

Read the implementation behind a feature before documenting it. Test the instructions from the reader's starting point, including how they get any IDs, credentials, or files the example needs.

Write for the person doing the work

  • Use everyday English, short paragraphs, and sentence-case headings.
  • Start with what the reader can accomplish and what they need first.
  • Use the actual labels in the interface. Explain a term before relying on it.
  • Avoid marketing filler, unnecessary warnings, and em dashes.
  • Explain the result of an action, plus how to change or undo it when relevant.
  • Keep setup tutorials separate from detailed configuration references.
  • Use example.com domains and fictional people. Never include live credentials or private project data.

Add useful images

Use real app screenshots with fictional data. Include both themes where available, descriptive alt text, and a caption that tells readers what to notice. Put the image next to the step it explains. Use diagrams for system relationships instead of inventing screenshots of an interface.

See the screenshot guide for reproduction. Verify the page at desktop and mobile widths, and check that the right screenshot appears in each theme.

Check your change

From apps/docs, run pnpm dlx mint validate and pnpm dlx mint broken-links. Preview with pnpm dlx mint dev and follow the updated navigation and instructions in a browser.

Keep existing page URLs where possible. When a URL must change, add a redirect in docs.json and update internal links. Add new guide pages to the appropriate navigation group.

API route and schema changes require pnpm openapi:check:fix from the repository root. Do not hand-edit generated endpoint contracts to conceal a mismatch. Older links under api-reference/endpoints/ are kept as thin pages pointing at the same schema; update their method and path when a route moves, and let OpenAPI supply their descriptions.

In your pull request, say which instructions you executed and which you only reviewed. A successful docs build does not prove an installation or restore works.


Apps Docs Docs.Json (`apps/docs/docs.json`)

{
"$schema": "https://mintlify.com/docs.json",
"theme": "almond",
"name": "Kaneo",
"colors": {
"primary": "#111111",
"light": "#fafafa",
"dark": "#1f1f1f"
},
"appearance": {
"default": "dark"
},
"icons": {
"library": "lucide"
},
"styling": {
"codeblocks": {
"theme": {
"light": "min-light",
"dark": "min-dark"
}
}
},
"fonts": {
"family": "Geist Variable",
"source": "https://kaneo.app/docs/fonts/geist.woff2",
"format": "woff2"
},
"favicon": "/favicon.svg",
"logo": {
"light": "/logo/dark.svg",
"dark": "/logo/light.svg"
},
"redirects": [
{
"source": "/core/guide/migrating-from-v1",
"destination": "/core/installation/environment-variables"
},
{
"source": "/core/integrations/cli",
"destination": "/cli"
},
{
"source": "/core/integrations/cli-commands",
"destination": "/cli/reference"
}
],
"navigation": {
"tabs": [
{
"tab": "Using Kaneo",
"groups": [
{
"group": "Start here",
"pages": [
"index",
"core/index",
"core/functional/index",
"core/functional/create-workspace-and-project"
]
},
{
"group": "Work with tasks",
"pages": [
"core/functional/plan-and-execute-tasks",
"core/functional/backlog-planning",
"core/functional/calendar-and-gantt",
"core/functional/configure-workflows",
"core/functional/manage-workspace-labels"
]
},
{
"group": "Your team and account",
"pages": [
"core/functional/collaborate-with-team",
"core/functional/account-notifications",
"core/functional/personalize-your-workspace",
"core/functional/manage-your-account"
]
},
{
"group": "Import from other tools",
"pages": [
"core/migrations/index",
"core/migrations/from-linear",
"core/migrations/from-jira",
"core/migrations/from-trello",
"core/migrations/from-plane",
"core/migrations/from-planka",
"core/migrations/from-kaneo"
]
}
]
},
{
"tab": "Self-hosting",
"groups": [
{
"group": "Install",
"pages": [
"core/installation/index",
"core/installation/docker-compose",
"core/installation/drim",
"core/deployments/coolify",
"core/deployments/railway"
]
},
{
"group": "Configure",
"pages": [
"core/reverse-proxy/nginx",
"core/installation/object-storage-and-image-uploads",
"core/installation/silo",
"core/installation/storage-backends",
"core/installation/environment-variables"
]
},
{
"group": "Sign-in providers",
"pages": [
"core/social-providers/github",
"core/social-providers/google",
"core/social-providers/discord",
"core/social-providers/custom-oauth"
]
},
{
"group": "Keep it running",
"pages": [
"core/operations/administration",
"core/operations/backups",
"core/operations/upgrades",
"core/operations/troubleshooting"
]
},
{
"group": "Migrate a deployment",
"pages": [
"core/installation/migration",
"core/installation/drim-migration"
]
}
]
},
{
"tab": "Integrations",
"groups": [
{
"group": "Overview",
"pages": ["core/integrations/index"]
},
{
"group": "Repositories",
"pages": [
"core/integrations/github/setup",
"core/integrations/github/configuration",
"core/integrations/github/troubleshooting",
"core/integrations/gitea/setup",
"core/integrations/gitlab/setup"
]
},
{
"group": "Team updates",
"pages": [
"core/integrations/slack",
"core/integrations/discord",
"core/integrations/mattermost",
"core/integrations/telegram",
"core/integrations/outgoing-webhooks"
]
},
{
"group": "Personal notifications",
"pages": [
"core/integrations/ntfy",
"core/integrations/gotify",
"core/integrations/personal-webhooks"
]
},
{
"group": "AI assistants",
"pages": ["core/integrations/mcp"]
}
]
},
{
"tab": "Developers",
"groups": [
{
"group": "Start building",
"pages": [
"api-reference/introduction",
"api-reference/authentication",
"api-reference/errors",
"core/integrations/mcp-stateless"
]
},
{
"group": "Command line",
"pages": [
"cli/index",
"cli/install",
"cli/sign-in",
"cli/configuration",
"cli/scripting",
"cli/reference"
]
},
{
"group": "Endpoint reference",
"openapi": "openapi.json"
}
]
}
],
"global": {
"anchors": [
{
"anchor": "Website",
"href": "https://kaneo.app",
"icon": "globe"
},
{
"anchor": "GitHub",
"href": "https://github.com/usekaneo/kaneo",
"icon": "github"
}
]
}
},
"navbar": {
"links": [
{
"label": "GitHub",
"href": "https://github.com/usekaneo/kaneo"
}
],
"primary": {
"type": "button",
"label": "Open Kaneo",
"href": "https://cloud.kaneo.app"
}
},
"contextual": {
"options": ["copy", "view", "chatgpt", "claude", "cursor"]
},
"footer": {
"socials": {
"website": "https://kaneo.app",
"github": "https://github.com/usekaneo/kaneo",
"x": "https://x.com/usekaneo"
}
},
"seo": {
"indexing": "all",
"metatags": {
"og:site_name": "Kaneo Docs",
"og:type": "website",
"og:image": "https://kaneo.app/images/hero.png",
"twitter:card": "summary_large_image",
"twitter:site": "@usekaneo",
"twitter:image": "https://kaneo.app/images/hero.png"
}
},
"integrations": {
"plausible": {
"domain": "kaneo.app",
"server": "plausible.kaneo.app"
}
}
}


Apps Docs Openapi.Json (`apps/docs/openapi.json`)

{
"openapi": "3.1.0",
"info": {
"title": "Kaneo API",
"version": "1.0.0",
"description": "Kaneo Project Management API - Manage projects, tasks, labels, and more"
},
"servers": [
{
"url": "https://cloud.kaneo.app/api",
"description": "Kaneo API Server"
}
],
"security": [
{
"bearerAuth": []
}
],
"components": {
"responses": {
"ApiKeyRateLimited": {
"description": "The API key exceeded its rate limit or usage quota. Wait the number of seconds in Retry-After before retrying. The X-RateLimit headers are sent only when the rate limit was hit.",
"headers": {
"Retry-After": {
"description": "Seconds to wait before retrying. Omitted when an exhausted usage quota never refills.",
"schema": {
"type": "integer"
}
},
"X-RateLimit-Limit": {
"description": "Requests the API key may make per window.",
"schema": {
"type": "integer"
}
},
"X-RateLimit-Remaining": {
"description": "Requests left in the current window.",
"schema": {
"type": "integer"
}
},
"X-RateLimit-Reset": {
"description": "Unix time in seconds when the current window resets.",
"schema": {
"type": "integer"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
},
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"description": "API key or session token (Bearer)"
}
},
"schemas": {
"ApiError": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Human-readable description of the error.",
"example": "Task not found"
},
"code": {
"type": "string",
"description": "Stable UPPER_SNAKE_CASE code. Derived from the status unless a more specific code applies, such as VALIDATION_ERROR or MISSING_PERMISSION.",
"example": "NOT_FOUND"
},
"issues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Dotted path to the invalid input, prefixed with the request part (body, query, params, header).",
"example": "body.title"
},
"message": {
"type": "string",
"example": "Required"
}
},
"required": [
"path",
"message"
]
},
"description": "Every validation issue, for VALIDATION_ERROR."
},
"missingPermissions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Permissions the caller lacks for this route, as resource:action, for MISSING_PERMISSION and API_KEY_SCOPE.",
"example": [
"task:update"
]
}
},
"required": [
"message",
"code"
]
},
"InstanceStatus": {
"type": "object",
"properties": {
"hasUsers": {
"type": "boolean"
},
"hasAdmin": {
"type": "boolean"
}
},
"required": [
"hasUsers",
"hasAdmin"
]
},
"PublicBoardPage": {
"allOf": [
{
"$ref": "#/components/schemas/Board"
},
{
"type": "object",
"properties": {
"pagination": {
"$ref": "#/components/schemas/BoardPagination"
}
},
"required": [
"pagination"
]
}
]
},
"BoardColumn": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "The column slug, same as slug."
},
"slug": {
"type": "string"
},
"name": {
"type": "string"
},
"icon": {
"type": [
"string",
"null"
]
},
"isFinal": {
"type": "boolean"
},
"position": {
"type": "number"
},
"tasks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BoardTask"
}
}
},
"required": [
"id",
"slug",
"name",
"icon",
"isFinal",
"tasks"
]
},
"BoardTask": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"number": {
"type": [
"number",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"descriptionDeferred": {
"type": "boolean",
"description": "True when the list omits a large description; load the task detail or description pages to read it. Do not replace stored text with this null summary."
},
"status": {
"type": "string"
},
"priority": {
"type": "string",
"description": "One of: no-priority, low, medium, high, urgent."
},
"startDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"dueDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"position": {
"type": [
"number",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"userId": {
"type": [
"string",
"null"
]
},
"assigneeName": {
"type": [
"string",
"null"
]
},
"assigneeId": {
"type": [
"string",
"null"
]
},
"assigneeImage": {
"type": [
"string",
"null"
]
},
"projectId": {
"type": "string"
},
"subtaskCounts": {
"type": "object",
"properties": {
"completed": {
"type": "integer",
"minimum": 0
},
"total": {
"type": "integer",
"minimum": 0
}
},
"required": [
"completed",
"total"
],
"description": "Direct subtasks in the workspace; completed means a final column in the child project. Public boards count only children in public projects."
},
"labels": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TaskLabel"
}
},
"externalLinks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TaskExternalLink"
}
}
},
"required": [
"id",
"title",
"number",
"description",
"status",
"priority",
"startDate",
"dueDate",
"position",
"createdAt",
"userId",
"assigneeName",
"assigneeId",
"assigneeImage",
"projectId",
"subtaskCounts",
"labels",
"externalLinks"
]
},
"TaskLabel": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"color": {
"type": "string"
}
},
"required": [
"id",
"name",
"color"
]
},
"TaskExternalLink": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"taskId": {
"type": "string"
},
"integrationId": {
"type": [
"string",
"null"
]
},
"resourceType": {
"type": "string"
},
"externalId": {
"type": "string"
},
"url": {
"type": "string"
},
"title": {
"type": [
"string",
"null"
]
},
"metadata": {
"type": [
"object",
"null"
],
"additionalProperties": {},
"description": "Provider-specific payload, already parsed from the stored JSON string."
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"taskId",
"integrationId",
"resourceType",
"externalId",
"url",
"title",
"metadata",
"createdAt",
"updatedAt"
]
},
"BoardPagination": {
"type": "object",
"properties": {
"total": {
"type": "number"
},
"page": {
"type": "number"
},
"pageSize": {
"type": "number"
},
"totalPages": {
"type": "number"
},
"relatedPage": {
"type": "number"
},
"relatedPageSize": {
"type": "number"
},
"relatedTotalPages": {
"type": "number"
},
"relatedRevision": {
"type": "string",
"description": "Public board labels and external links revision for this task page. Restart pagination if it changes during related-page continuations."
},
"revision": {
"type": "string",
"description": "Public board content, membership and ordering revision, including related records and visible subtask progress. Restart pagination if it changes between task or related pages."
}
},
"required": [
"total",
"page",
"pageSize",
"totalPages",
"relatedPage",
"relatedPageSize",
"relatedTotalPages"
],
"description": "Always paginated: 50 tasks by default, at most 100 per page. Continue through totalPages to retrieve all tasks. For each task page, follow relatedPage through relatedTotalPages to retrieve all labels, external links and columns (100 related rows per kind per request, plus up to 100 columns needed to represent the tasks)."
},
"Board": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"slug": {
"type": "string"
},
"icon": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"descriptionDeferred": {
"type": "boolean",
"description": "True when a project description above 64 KiB is omitted; use project detail or public project description pages for full text."
},
"isPublic": {
"type": [
"boolean",
"null"
]
},
"workspaceId": {
"type": "string"
},
"backgroundVersion": {
"type": [
"string",
"null"
]
},
"columns": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BoardColumn"
}
},
"archivedTasks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BoardTask"
}
},
"plannedTasks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BoardTask"
}
}
},
"required": [
"id",
"name",
"slug",
"icon",
"description",
"isPublic",
"workspaceId",
"backgroundVersion",
"columns",
"archivedTasks",
"plannedTasks"
]
},
"TaskDescriptionPage": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"version": {
"type": "string"
},
"nextOffset": {
"type": [
"number",
"null"
]
}
},
"required": [
"content",
"version",
"nextOffset"
]
},
"Config": {
"type": "object",
"properties": {
"isCloud": {
"type": "boolean"
},
"disableRegistration": {
"type": "boolean"
},
"disablePasswordRegistration": {
"type": "boolean"
},
"disableEmailOtpSignIn": {
"type": "boolean"
},
"disableWorkspaceCreation": {
"type": "boolean"
},
"isDemoMode": {
"type": "boolean"
},
"hasSmtp": {
"type": "boolean"
},
"hasImageUploads": {
"type": "boolean",
"description": "Whether S3-compatible storage is configured, so task images and board backgrounds can be uploaded."
},
"hasGithubSignIn": {
"type": "boolean"
},
"hasGoogleSignIn": {
"type": "boolean"
},
"hasDiscordSignIn": {
"type": "boolean"
},
"hasCustomOAuth": {
"type": "boolean"
},
"hasGuestAccess": {
"type": "boolean"
},
"disableLoginForm": {
"type": "boolean"
},
"customOAuthName": {
"type": [
"string",
"null"
],
"description": "Label for the custom OAuth provider on the sign-in page. Null means the generic SSO label."
},
"customOAuthAutoLogin": {
"type": "boolean"
},
"customOAuthLogoutUrl": {
"type": [
"string",
"null"
]
},
"billingEnabled": {
"type": "boolean"
}
},
"required": [
"isCloud",
"disableRegistration",
"disablePasswordRegistration",
"disableEmailOtpSignIn",
"disableWorkspaceCreation",
"isDemoMode",
"hasSmtp",
"hasImageUploads",
"hasGithubSignIn",
"hasGoogleSignIn",
"hasDiscordSignIn",
"hasCustomOAuth",
"hasGuestAccess",
"disableLoginForm",
"customOAuthName",
"customOAuthAutoLogin",
"customOAuthLogoutUrl",
"billingEnabled"
]
},
"McpClientRegistration": {
"type": "object",
"properties": {
"client_id": {
"type": "string"
},
"client_id_issued_at": {
"type": "number"
},
"redirect_uris": {
"type": "array",
"items": {
"type": "string"
}
},
"client_name": {
"type": "string"
},
"token_endpoint_auth_method": {
"type": "string",
"enum": [
"none"
]
},
"grant_types": {
"type": "array",
"items": {
"type": "string",
"enum": [
"authorization_code"
]
}
},
"response_types": {
"type": "array",
"items": {
"type": "string",
"enum": [
"code"
]
}
}
},
"required": [
"client_id",
"client_id_issued_at",
"redirect_uris",
"token_endpoint_auth_method",
"grant_types",
"response_types"
]
},
"OAuthError": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"error_description": {
"type": "string"
}
},
"required": [
"error"
]
},
"McpAuthorizationRequest": {
"type": "object",
"properties": {
"client_name": {
"type": "string"
},
"redirect_uri": {
"type": "string"
}
},
"required": [
"client_name",
"redirect_uri"
]
},
"McpAuthorizationDecision": {
"type": "object",
"properties": {
"redirect": {
"type": "string",
"description": "Where to send the browser next: back to the client with a code on approval, or with an error on denial."
}
},
"required": [
"redirect"
]
},
"OAuthIdToken": {
"type": "object",
"properties": {
"idToken": {
"type": [
"string",
"null"
],
"description": "The stored id_token for the user's custom OAuth account, or null when they have no such account."
}
},
"required": [
"idToken"
]
},
"MovedProject": {
"allOf": [
{
"$ref": "#/components/schemas/Project"
},
{
"type": "object",
"properties": {
"unassignedTaskCount": {
"type": "number"
}
},
"required": [
"unassignedTaskCount"
]
}
]
},
"Project": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"workspaceId": {
"type": "string"
},
"backgroundVersion": {
"type": [
"string",
"null"
]
},
"slug": {
"type": "string",
"description": "Short prefix used in task identifiers, e.g. KAN-12."
},
"icon": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"isPublic": {
"type": [
"boolean",
"null"
],
"description": "When true the project's board is readable without signing in, via /api/public-project/{id}."
},
"archivedAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "Non-null once archived; archived projects are hidden by default."
},
"position": {
"type": "number",
"description": "Sidebar order, ascending."
},
"lastTaskNumber": {
"type": "number",
"description": "Highest task number issued in this project; the next task gets this plus one."
}
},
"required": [
"id",
"workspaceId",
"backgroundVersion",
"slug",
"icon",
"name",
"description",
"createdAt",
"isPublic",
"archivedAt",
"position",
"lastTaskNumber"
]
},
"ProjectListItem": {
"allOf": [
{
"$ref": "#/components/schemas/Project"
},
{
"type": "object",
"properties": {
"statistics": {
"$ref": "#/components/schemas/ProjectStatistics"
},
"archivedTasks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BoardTask"
},
"description": "Always empty."
},
"plannedTasks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BoardTask"
},
"description": "Always empty."
},
"columns": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BoardColumn"
},
"description": "Always empty."
}
},
"required": [
"statistics",
"archivedTasks",
"plannedTasks",
"columns"
]
}
]
},
"ProjectStatistics": {
"type": "object",
"properties": {
"completionPercentage": {
"type": "number"
},
"totalTasks": {
"type": "number"
},
"dueDate": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "The soonest due date among the project's open tasks."
}
},
"required": [
"completionPercentage",
"totalTasks",
"dueDate"
]
},
"ProjectBackgroundUpload": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"uploadUrl": {
"type": "string"
},
"version": {
"type": "string"
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"key",
"uploadUrl",
"version",
"headers"
]
},
"ProjectBackgroundFinalize": {
"type": "object",
"properties": {
"url": {
"type": "string"
}
},
"required": [
"url"
]
},
"CalendarFeed": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"token": {
"type": "string"
},
"labelIds": {
"type": "array",
"items": {
"type": "string"
}
},
"timeZone": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"token",
"labelIds",
"timeZone",
"createdAt"
]
},
"TaskDescriptionMatches": {
"type": "object",
"properties": {
"ids": {
"type": "array",
"items": {
"type": "string"
}
},
"nextCursor": {
"type": [
"string",
"null"
]
}
},
"required": [
"ids",
"nextCursor"
]
},
"AssignedTasks": {
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AssignedTask"
}
},
"total": {
"type": "number",
"description": "All open tasks assigned to the caller, including any beyond the first 100 returned."
}
},
"required": [
"tasks",
"total"
]
},
"AssignedTask": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"number": {
"type": [
"number",
"null"
]
},
"title": {
"type": "string"
},
"status": {
"type": "string",
"description": "The slug of the column the task sits in."
},
"statusName": {
"type": [
"string",
"null"
],
"description": "The column's display name. Null for tasks outside a column, such as planned ones."
},
"statusIcon": {
"type": [
"string",
"null"
]
},
"priority": {
"type": "string",
"description": "One of: no-priority, low, medium, high, urgent."
},
"dueDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"projectName": {
"type": "string"
},
"projectSlug": {
"type": "string"
},
"projectIcon": {
"type": [
"string",
"null"
]
},
"labels": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TaskLabel"
}
}
},
"required": [
"id",
"projectId",
"number",
"title",
"status",
"statusName",
"statusIcon",
"priority",
"dueDate",
"projectName",
"projectSlug",
"projectIcon",
"labels"
]
},
"BoardResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/components/schemas/Board"
},
"pagination": {
"$ref": "#/components/schemas/BoardPagination"
}
},
"required": [
"data",
"pagination"
]
},
"BulkTaskResult": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"updatedCount": {
"type": "number"
}
},
"required": [
"success",
"updatedCount"
]
},
"TaskImageUpload": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Object key to send back to the finalize route."
},
"uploadUrl": {
"type": "string",
"description": "Presigned URL to PUT the image bytes to."
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Headers that must accompany the upload request, including Content-Type."
}
},
"required": [
"key",
"uploadUrl",
"headers"
]
},
"TaskImageAsset": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"url": {
"type": "string",
"description": "Where the stored image can be read back from."
}
},
"required": [
"id",
"url"
]
},
"Task": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"position": {
"type": [
"number",
"null"
],
"description": "Order within its column, ascending."
},
"number": {
"type": [
"number",
"null"
],
"description": "Per-project counter shown as {projectSlug}-{number}."
},
"userId": {
"type": [
"string",
"null"
],
"description": "The assignee, if any."
},
"title": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"descriptionDeferred": {
"type": "boolean",
"description": "True when the list omits a large description; load the task detail or description pages to read it. Do not replace stored text with this null summary."
},
"status": {
"type": "string",
"description": "The slug of the column the task sits in."
},
"priority": {
"type": "string",
"description": "One of: no-priority, low, medium, high, urgent."
},
"startDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"dueDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"customFields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"fieldId": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"fieldId",
"value"
]
}
}
},
"required": [
"id",
"projectId",
"position",
"number",
"userId",
"title",
"description",
"status",
"priority",
"startDate",
"dueDate",
"createdAt"
]
},
"TaskByTicketId": {
"allOf": [
{
"$ref": "#/components/schemas/TaskWithAssignee"
},
{
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "The workspace that owns the task's project."
}
},
"required": [
"workspaceId"
]
}
]
},
"TaskWithAssignee": {
"allOf": [
{
"$ref": "#/components/schemas/Task"
},
{
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "The workspace currently owning the task's project. Included in the detail view; omitted from the compact board view."
},
"columnId": {
"type": [
"string",
"null"
],
"description": "The referenced workflow column; null for virtual statuses and legacy tasks without a column reference."
},
"subtaskCounts": {
"type": "object",
"properties": {
"completed": {
"type": "number"
},
"total": {
"type": "number"
}
},
"required": [
"completed",
"total"
]
},
"parentSubtaskCounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"taskId": {
"type": "string"
},
"completed": {
"type": "number"
},
"total": {
"type": "number"
}
},
"required": [
"taskId",
"completed",
"total"
]
}
},
"assigneeName": {
"type": [
"string",
"null"
]
},
"assigneeId": {
"type": [
"string",
"null"
]
}
},
"required": [
"columnId",
"assigneeName",
"assigneeId"
]
}
]
},
"MoveTaskResult": {
"type": "object",
"properties": {
"task": {
"$ref": "#/components/schemas/Task"
},
"sourceProjectId": {
"type": "string"
},
"destinationProjectId": {
"type": "string"
}
},
"required": [
"task",
"sourceProjectId",
"destinationProjectId"
]
},
"TaskExport": {
"type": "object",
"properties": {
"project": {
"$ref": "#/components/schemas/TaskExportProject"
},
"tasks": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ExportedTask"
}
}
},
"required": [
"project",
"tasks"
]
},
"TaskExportProject": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": [
"string",
"null"
]
},
"exportedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"name",
"slug",
"description",
"exportedAt"
]
},
"ExportedTask": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"status": {
"type": "string"
},
"priority": {
"type": "string"
},
"dueDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"startDate": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"userId": {
"type": [
"string",
"null"
]
},
"labels": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ExportedTaskLabel"
},
"description": "Label names and colors, without their ids."
}
},
"required": [
"title",
"description",
"status",
"priority",
"dueDate",
"startDate",
"userId",
"labels"
]
},
"ExportedTaskLabel": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"color": {
"type": "string"
}
},
"required": [
"name",
"color"
]
},
"TaskImportResult": {
"type": "object",
"properties": {
"results": {
"$ref": "#/components/schemas/TaskImportSummary"
}
},
"required": [
"results"
]
},
"TaskImportSummary": {
"type": "object",
"properties": {
"total": {
"type": "number"
},
"successful": {
"type": "number"
},
"failed": {
"type": "number"
},
"tasks": {
"type": "array",
"items": {},
"description": "Per-task outcome, each carrying a success flag."
}
},
"required": [
"total",
"successful",
"failed",
"tasks"
]
},
"Column": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"name": {
"type": "string"
},
"slug": {
"type": "string",
"description": "Stable identifier derived from the name; tasks store this as their status."
},
"position": {
"type": "number",
"description": "Board order, ascending. Columns are always returned sorted by it."
},
"icon": {
"type": [
"string",
"null"
]
},
"color": {
"type": [
"string",
"null"
]
},
"isFinal": {
"type": "boolean",
"description": "Marks the column as a done state, which stops overdue reminders for tasks in it."
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"name",
"slug",
"position",
"icon",
"color",
"isFinal",
"createdAt",
"updatedAt"
]
},
"WorkspaceActivity": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"description": "One of: comment, task, create, status_changed, priority_changed, assignee_changed, unassigned, due_date_changed, title_changed, description_changed."
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"eventData": {
"description": "Type-specific payload. For status_changed, oldStatus/newStatus are slugs and oldStatusName/newStatusName carry current configured column names when available."
},
"excerpt": {
"type": [
"string",
"null"
],
"description": "Plain-text preview of the stored content, at most 240 characters: the comment body, or the description older system events carry instead of eventData."
},
"userId": {
"type": [
"string",
"null"
]
},
"userName": {
"type": [
"string",
"null"
]
},
"userImage": {
"type": [
"string",
"null"
]
},
"externalUserName": {
"type": [
"string",
"null"
]
},
"externalUserAvatar": {
"type": [
"string",
"null"
]
},
"taskId": {
"type": "string"
},
"taskTitle": {
"type": "string"
},
"taskNumber": {
"type": [
"number",
"null"
]
},
"projectId": {
"type": "string"
},
"projectSlug": {
"type": "string"
}
},
"required": [
"id",
"type",
"createdAt",
"excerpt",
"userId",
"userName",
"userImage",
"externalUserName",
"externalUserAvatar",
"taskId",
"taskTitle",
"taskNumber",
"projectId",
"projectSlug"
]
},
"Activity": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"taskId": {
"type": "string"
},
"type": {
"type": "string",
"description": "One of: comment, task, create, status_changed, priority_changed, assignee_changed, unassigned, due_date_changed, title_changed, description_changed."
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"userId": {
"type": [
"string",
"null"
]
},
"content": {
"type": [
"string",
"null"
]
},
"eventData": {
"description": "Type-specific payload, e.g. { oldStatus, newStatus } for status_changed. Null for plain comments."
},
"externalUserName": {
"type": [
"string",
"null"
],
"description": "Set when the activity was imported from another tool."
},
"externalUserAvatar": {
"type": [
"string",
"null"
]
},
"externalSource": {
"type": [
"string",
"null"
],
"description": "The tool it was imported from: planka, trello, jira, plane, linear, kaneo, or a connected forge such as github."
},
"externalUrl": {
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"taskId",
"type",
"createdAt",
"updatedAt",
"userId",
"content",
"externalUserName",
"externalUserAvatar",
"externalSource",
"externalUrl"
]
},
"Comment": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"taskId": {
"type": "string"
},
"userId": {
"type": "string"
},
"content": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"user": {
"$ref": "#/components/schemas/CommentAuthor"
}
},
"required": [
"id",
"taskId",
"userId",
"content",
"createdAt",
"updatedAt",
"user"
]
},
"CommentAuthor": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"image": {
"type": [
"string",
"null"
]
}
},
"required": [
"name",
"image"
]
},
"TimeEntryWithUser": {
"allOf": [
{
"$ref": "#/components/schemas/TimeEntry"
},
{
"type": "object",
"properties": {
"userName": {
"type": [
"string",
"null"
]
}
},
"required": [
"userName"
]
}
]
},
"TimeEntry": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"taskId": {
"type": "string"
},
"userId": {
"type": [
"string",
"null"
],
"description": "Null once the user who logged the time has been removed."
},
"description": {
"type": [
"string",
"null"
]
},
"startTime": {
"type": "string",
"format": "date-time"
},
"endTime": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "Null while the timer is still running."
},
"duration": {
"type": [
"number",
"null"
],
"description": "Elapsed seconds, filled in once the entry has an endTime."
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"taskId",
"userId",
"description",
"startTime",
"endTime",
"duration",
"createdAt",
"updatedAt"
]
},
"Label": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"color": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"deletionStartedAt": {
"type": "string",
"format": "date-time"
},
"taskId": {
"type": [
"string",
"null"
]
},
"workspaceId": {
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"name",
"color",
"createdAt",
"updatedAt",
"taskId",
"workspaceId"
]
},
"PendingLabelDeletion": {
"allOf": [
{
"$ref": "#/components/schemas/Label"
},
{
"type": "object",
"properties": {
"pendingDeletion": {
"type": "boolean",
"enum": [
true
]
}
},
"required": [
"pendingDeletion"
]
}
]
},
"Notification": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"userId": {
"type": "string"
},
"title": {
"type": [
"string",
"null"
]
},
"content": {
"type": [
"string",
"null"
],
"description": "Free-text body. Null for generated notifications, whose text the client renders from type and eventData."
},
"type": {
"type": "string",
"description": "One of: info, task_created, workspace_created, task_status_changed, task_assignee_changed, time_entry_created, due_date_reminder, task_overdue, task_mention, task_comment."
},
"eventData": {
"description": "Type-specific payload, e.g. { taskTitle, oldStatus, newStatus, projectId, workspaceId }."
},
"isRead": {
"type": [
"boolean",
"null"
]
},
"resourceId": {
"type": [
"string",
"null"
],
"description": "The id of the task or workspace the notification points at."
},
"resourceType": {
"type": [
"string",
"null"
],
"description": "task or workspace."
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"userId",
"title",
"content",
"type",
"isRead",
"resourceId",
"resourceType",
"createdAt",
"updatedAt"
]
},
"NotificationBulkResult": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"required": [
"success"
]
},
"NotificationPreferences": {
"type": "object",
"properties": {
"emailAddress": {
"type": [
"string",
"null"
]
},
"emailEnabled": {
"type": "boolean"
},
"ntfyEnabled": {
"type": "boolean"
},
"ntfyConfigured": {
"type": "boolean"
},
"ntfyServerUrl": {
"type": [
"string",
"null"
]
},
"ntfyTopic": {
"type": [
"string",
"null"
]
},
"ntfyTokenConfigured": {
"type": "boolean"
},
"maskedNtfyToken": {
"type": [
"string",
"null"
]
},
"gotifyEnabled": {
"type": "boolean"
},
"gotifyConfigured": {
"type": "boolean"
},
"gotifyServerUrl": {
"type": [
"string",
"null"
]
},
"gotifyTokenConfigured": {
"type": "boolean"
},
"maskedGotifyToken": {
"type": [
"string",
"null"
]
},
"webhookEnabled": {
"type": "boolean"
},
"webhookConfigured": {
"type": "boolean"
},
"webhookUrl": {
"type": [
"string",
"null"
]
},
"webhookSecretConfigured": {
"type": "boolean"
},
"maskedWebhookSecret": {
"type": [
"string",
"null"
]
},
"taskAssignmentEnabled": {
"type": "boolean"
},
"taskCommentEnabled": {
"type": "boolean"
},
"taskStatusChangeEnabled": {
"type": "boolean"
},
"dueDateReminderEnabled": {
"type": "boolean"
},
"dueDateReminderLeadTimeMinutes": {
"type": "number",
"description": "How long before a due date the reminder fires, in minutes."
},
"workspaces": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NotificationPreferenceWorkspaceRule"
},
"description": "Per-workspace overrides. A workspace with no rule follows the global settings."
},
"createdAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"updatedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
"required": [
"emailAddress",
"emailEnabled",
"ntfyEnabled",
"ntfyConfigured",
"ntfyServerUrl",
"ntfyTopic",
"ntfyTokenConfigured",
"maskedNtfyToken",
"gotifyEnabled",
"gotifyConfigured",
"gotifyServerUrl",
"gotifyTokenConfigured",
"maskedGotifyToken",
"webhookEnabled",
"webhookConfigured",
"webhookUrl",
"webhookSecretConfigured",
"maskedWebhookSecret",
"taskAssignmentEnabled",
"taskCommentEnabled",
"taskStatusChangeEnabled",
"dueDateReminderEnabled",
"dueDateReminderLeadTimeMinutes",
"workspaces",
"createdAt",
"updatedAt"
]
},
"NotificationPreferenceWorkspaceRule": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"workspaceId": {
"type": "string"
},
"workspaceName": {
"type": "string"
},
"isActive": {
"type": "boolean",
"description": "Turn all notifications for this workspace on or off."
},
"emailEnabled": {
"type": "boolean"
},
"ntfyEnabled": {
"type": "boolean"
},
"gotifyEnabled": {
"type": "boolean"
},
"webhookEnabled": {
"type": "boolean"
},
"projectMode": {
"type": "string",
"enum": [
"all",
"selected"
],
"description": "all notifies for every project in the workspace; selected restricts it to selectedProjectIds."
},
"selectedProjectIds": {
"type": "array",
"items": {
"type": "string"
}
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"workspaceId",
"workspaceName",
"isActive",
"emailEnabled",
"ntfyEnabled",
"gotifyEnabled",
"webhookEnabled",
"projectMode",
"selectedProjectIds",
"createdAt",
"updatedAt"
]
},
"SearchResponse": {
"type": "object",
"properties": {
"results": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SearchResult"
}
},
"totalCount": {
"type": "number",
"description": "Total matches found before the limit slice, so a caller can tell that more exist."
},
"searchQuery": {
"type": "string",
"description": "The query that was run."
}
},
"required": [
"results",
"totalCount",
"searchQuery"
]
},
"SearchResult": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"task",
"project",
"workspace",
"comment",
"activity"
]
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"content": {
"type": "string",
"description": "Matched body text, for comment and activity results. Activities get a rendered summary such as changed status from Todo to In Progress."
},
"projectId": {
"type": "string"
},
"projectName": {
"type": "string"
},
"projectSlug": {
"type": "string"
},
"workspaceId": {
"type": "string"
},
"workspaceName": {
"type": "string"
},
"userId": {
"type": "string"
},
"userName": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"relevanceScore": {
"type": "number",
"description": "Higher is a better match. Results are sorted by score, then by createdAt descending."
},
"taskNumber": {
"type": "number"
},
"priority": {
"type": "string"
},
"status": {
"type": "string"
}
},
"required": [
"id",
"type",
"title",
"createdAt",
"relevanceScore"
]
},
"IntegrationSyncPreview": {
"type": "object",
"properties": {
"isActive": {
"type": "boolean"
},
"rules": {
"type": "object",
"properties": {
"outgoing": {
"oneOf": [
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": [
"all"
]
}
},
"required": [
"mode"
]
},
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": [
"labels"
]
},
"match": {
"type": "string",
"enum": [
"any",
"all"
]
},
"labels": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"minItems": 1,
"maxItems": 50
}
},
"required": [
"mode",
"match",
"labels"
]
}
],
"description": "Kaneo workspace label IDs controlling task sync. Missing labels pause sync."
},
"incoming": {
"oneOf": [
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": [
"all"
]
}
},
"required": [
"mode"
]
},
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": [
"labels"
]
},
"match": {
"type": "string",
"enum": [
"any",
"all"
]
},
"labels": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"minItems": 1,
"maxItems": 50
}
},
"required": [
"mode",
"match",
"labels"
]
}
],
"description": "Exact repository label names controlling which issues can be imported."
}
},
"required": [
"outgoing",
"incoming"
]
},
"labels": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"color": {
"type": "string"
}
},
"required": [
"id",
"name",
"color"
]
}
},
"missingLabels": {
"type": "array",
"items": {
"type": "string"
}
},
"total": {
"type": "number"
},
"matching": {
"type": "number"
},
"willCreate": {
"type": "number"
},
"willPause": {
"type": "number"
},
"needsReview": {
"type": "number"
},
"paused": {
"type": "number"
},
"matchingTasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"number": {
"type": [
"number",
"null"
]
},
"title": {
"type": "string"
}
},
"required": [
"id",
"number",
"title"
]
}
},
"pausedNextCursor": {
"type": [
"string",
"null"
]
},
"pausedTasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"number": {
"type": [
"number",
"null"
]
},
"title": {
"type": "string"
},
"linkId": {
"type": "string"
},
"url": {
"type": "string"
},
"eligible": {
"type": "boolean"
}
},
"required": [
"id",
"number",
"title",
"linkId",
"url",
"eligible"
]
}
},
"previewToken": {
"type": "string"
}
},
"required": [
"isActive",
"rules",
"labels",
"missingLabels",
"total",
"matching",
"willCreate",
"willPause",
"needsReview",
"paused",
"matchingTasks",
"pausedNextCursor",
"pausedTasks",
"previewToken"
]
},
"IntegrationSyncResumePreview": {
"type": "object",
"properties": {
"task": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"number": {
"type": [
"number",
"null"
]
},
"title": {
"type": "string"
}
},
"required": [
"id",
"number",
"title"
]
},
"local": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"open",
"closed"
]
}
},
"required": [
"title",
"description",
"state"
]
},
"remote": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"open",
"closed"
]
}
},
"required": [
"title",
"description",
"state"
]
},
"token": {
"type": "string"
}
},
"required": [
"task",
"local",
"remote",
"token"
]
},
"GitHubAppInfo": {
"type": "object",
"properties": {
"accountConnected": {
"type": "boolean"
},
"accountLinkingAvailable": {
"type": "boolean"
},
"appName": {
"type": [
"string",
"null"
],
"description": "The configured GitHub App slug, or null when this instance has no App set up."
}
},
"required": [
"accountConnected",
"accountLinkingAvailable",
"appName"
]
},
"GitHubRepositoryList": {
"type": "object",
"properties": {
"repositories": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GitHubRepository"
},
"description": "Repositories the caller administers, deduplicated within the response."
},
"installations": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GitHubInstallation"
}
},
"total": {
"type": "number"
},
"nextPage": {
"type": [
"object",
"null"
],
"properties": {
"installationPage": {
"type": "number"
},
"repositoryPage": {
"type": "number"
}
},
"required": [
"installationPage",
"repositoryPage"
],
"description": "Where to continue listing, or null when every installation has been read."
}
},
"required": [
"repositories",
"installations",
"total",
"nextPage"
]
},
"GitHubRepository": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string"
},
"full_name": {
"type": "string"
},
"private": {
"type": "boolean"
},
"owner": {
"$ref": "#/components/schemas/GitHubRepositoryOwner"
},
"description": {
"type": [
"string",
"null"
]
},
"html_url": {
"type": "string"
},
"permissions": {
"$ref": "#/components/schemas/GitHubRepositoryPermissions"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"installation_id": {
"type": "number",
"description": "Which App installation this repository came through."
}
},
"required": [
"id",
"name",
"full_name",
"private",
"owner",
"description",
"html_url",
"updated_at",
"installation_id"
]
},
"GitHubRepositoryOwner": {
"type": "object",
"properties": {
"login": {
"type": "string"
},
"avatar_url": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"login",
"avatar_url",
"type"
]
},
"GitHubRepositoryPermissions": {
"type": "object",
"properties": {
"admin": {
"type": "boolean"
},
"push": {
"type": "boolean"
},
"pull": {
"type": "boolean"
}
},
"required": [
"admin",
"push",
"pull"
]
},
"GitHubInstallation": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"account": {
"$ref": "#/components/schemas/GitHubInstallationAccount"
},
"repositories": {
"type": "array",
"items": {
"type": "string"
},
"description": "Full names of the repositories under this installation. Empty when listing them failed."
}
},
"required": [
"id",
"account",
"repositories"
]
},
"GitHubInstallationAccount": {
"type": [
"object",
"null"
],
"properties": {
"login": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"login",
"type"
]
},
"GitHubVerificationResult": {
"type": "object",
"properties": {
"isInstalled": {
"type": "boolean"
},
"installationId": {
"type": [
"number",
"null"
]
},
"repositoryExists": {
"type": [
"boolean",
"null"
]
},
"repositoryPrivate": {
"type": [
"boolean",
"null"
]
},
"permissions": {
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "string"
},
"description": "The permissions the installation currently grants."
},
"hasRequiredPermissions": {
"type": "boolean"
},
"missingPermissions": {
"type": "array",
"items": {
"type": "string"
},
"description": "Permissions that must be granted before the link will work."
},
"message": {
"type": "string",
"description": "A human-readable summary to show the user."
},
"settingsUrl": {
"type": "string",
"description": "Where to adjust the installation, when one exists."
},
"installationUrl": {
"type": "string",
"description": "Where to install the App, when it is not installed yet."
}
},
"required": [
"isInstalled",
"installationId",
"repositoryExists",
"repositoryPrivate",
"permissions",
"hasRequiredPermissions",
"missingPermissions",
"message"
]
},
"GitHubIntegration": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"repositoryOwner": {
"type": "string"
},
"repositoryName": {
"type": "string"
},
"installationId": {
"type": [
"number",
"null"
],
"description": "The GitHub App installation that grants access to the repository."
},
"requiresVerification": {
"type": "boolean"
},
"importProgress": {
"$ref": "#/components/schemas/IssueImportResult"
},
"branchPattern": {
"type": "string",
"description": "Template used to name branches created for a task."
},
"commentTaskLinkOnGitHubIssue": {
"type": "boolean",
"description": "When on, Kaneo comments a link back to the task on the linked GitHub issue."
},
"isActive": {
"type": [
"boolean",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"repositoryOwner",
"repositoryName",
"installationId",
"requiresVerification",
"isActive",
"createdAt",
"updatedAt"
]
},
"IssueImportResult": {
"type": "object",
"properties": {
"runId": {
"type": "string"
},
"pending": {
"type": "boolean"
},
"imported": {
"type": "number"
},
"updated": {
"type": "number"
},
"skipped": {
"type": "number",
"description": "Issues whose linked task or source disappeared or belongs to another project."
}
},
"required": [
"runId",
"pending",
"imported",
"updated",
"skipped"
]
},
"CreatedGitHubIntegration": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"repositoryOwner": {
"type": "string"
},
"repositoryName": {
"type": "string"
},
"installationId": {
"type": [
"number",
"null"
],
"description": "The GitHub App installation that grants access to the repository."
},
"requiresVerification": {
"type": "boolean"
},
"importProgress": {
"$ref": "#/components/schemas/IssueImportResult"
},
"branchPattern": {
"type": "string",
"description": "Template used to name branches created for a task."
},
"commentTaskLinkOnGitHubIssue": {
"type": "boolean",
"description": "When on, Kaneo comments a link back to the task on the linked GitHub issue."
},
"isActive": {
"type": [
"boolean",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"repositoryOwner",
"repositoryName",
"installationId",
"requiresVerification"
]
},
"GitHubDeleteResult": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"message": {
"type": "string"
}
},
"required": [
"success",
"message"
]
},
"GiteaRepositoryList": {
"type": "object",
"properties": {
"repositories": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GiteaRepository"
}
}
},
"required": [
"repositories"
]
},
"GiteaRepository": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string"
},
"full_name": {
"type": "string"
},
"owner": {
"$ref": "#/components/schemas/GiteaRepositoryOwner"
},
"private": {
"type": "boolean"
},
"html_url": {
"type": "string"
}
},
"required": [
"id",
"name",
"full_name",
"owner",
"private",
"html_url"
]
},
"GiteaRepositoryOwner": {
"type": "object",
"properties": {
"login": {
"type": "string"
}
},
"required": [
"login"
]
},
"GiteaVerificationResult": {
"type": "object",
"properties": {
"isInstalled": {
"type": "boolean"
},
"hasRequiredPermissions": {
"type": "boolean"
},
"repositoryExists": {
"type": "boolean"
},
"repositoryPrivate": {
"type": [
"boolean",
"null"
]
},
"missingPermissions": {
"type": "array",
"items": {
"type": "string"
}
},
"message": {
"type": "string",
"description": "A human-readable summary to show the user."
},
"failureReason": {
"type": [
"string",
"null"
],
"enum": [
"not_a_gitea_instance",
"redirected",
"repository_not_found",
null
],
"description": "Why verification failed, when it did. redirected usually means the base URL is behind a proxy that rewrites it."
}
},
"required": [
"isInstalled",
"hasRequiredPermissions",
"repositoryExists",
"repositoryPrivate",
"missingPermissions",
"message",
"failureReason"
]
},
"GiteaIntegration": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"baseUrl": {
"type": "string",
"description": "Root URL of the Gitea instance."
},
"repositoryOwner": {
"type": "string"
},
"repositoryName": {
"type": "string"
},
"maskedAccessToken": {
"type": "string"
},
"webhookUrl": {
"type": "string",
"description": "Where Gitea should POST events for this project."
},
"webhookSecret": {
"type": "string",
"description": "Only returned to callers with workspace:manage_settings, so the value can be pasted into Gitea."
},
"branchPattern": {
"type": "string"
},
"commentTaskLinkOnGiteaIssue": {
"type": "boolean",
"description": "When on, Kaneo comments a link back to the task on the linked Gitea issue."
},
"isActive": {
"type": [
"boolean",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"baseUrl",
"repositoryOwner",
"repositoryName",
"maskedAccessToken",
"isActive",
"createdAt",
"updatedAt"
]
},
"GiteaDeleteResult": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"message": {
"type": "string"
}
},
"required": [
"success",
"message"
]
},
"GiteaImportResult": {
"type": "object",
"properties": {
"imported": {
"type": "number"
},
"updated": {
"type": "number",
"description": "Existing tasks that were refreshed from their issue."
},
"skipped": {
"type": "number"
},
"errors": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"imported",
"updated",
"skipped"
]
},
"GitlabProjectList": {
"type": "object",
"properties": {
"projects": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GitlabProject"
}
}
},
"required": [
"projects"
]
},
"GitlabProject": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string"
},
"path_with_namespace": {
"type": "string"
},
"name_with_namespace": {
"type": "string"
},
"visibility": {
"type": "string"
},
"web_url": {
"type": "string"
}
},
"required": [
"id",
"name",
"path_with_namespace",
"name_with_namespace",
"visibility",
"web_url"
]
},
"GitlabVerificationResult": {
"type": "object",
"properties": {
"isInstalled": {
"type": "boolean",
"description": "The base URL answered as a GitLab instance for this token."
},
"hasRequiredPermissions": {
"type": "boolean"
},
"projectExists": {
"type": "boolean"
},
"projectVisibility": {
"type": [
"string",
"null"
]
},
"missingPermissions": {
"type": "array",
"items": {
"type": "string"
}
},
"message": {
"type": "string",
"description": "A human-readable summary to show the user."
},
"failureReason": {
"type": [
"string",
"null"
],
"enum": [
"not_a_gitlab_instance",
"redirected",
"project_not_found",
null
],
"description": "Why verification failed, when it did. redirected usually means the base URL is behind a proxy that rewrites it."
}
},
"required": [
"isInstalled",
"hasRequiredPermissions",
"projectExists",
"projectVisibility",
"missingPermissions",
"message",
"failureReason"
]
},
"GitlabIntegration": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"baseUrl": {
"type": "string",
"description": "Root URL of the GitLab instance."
},
"projectPath": {
"type": "string",
"description": "Full path of the linked project, nested groups included."
},
"tokenType": {
"type": "string",
"enum": [
"private",
"bearer"
]
},
"maskedAccessToken": {
"type": "string",
"description": "First and last characters of the stored token, only returned to callers with workspace:manage_settings."
},
"webhookUrl": {
"type": "string",
"description": "Where GitLab should POST events for this project."
},
"webhookSecret": {
"type": "string",
"description": "Only returned to callers with workspace:manage_settings, so the value can be pasted into GitLab."
},
"branchPattern": {
"type": "string"
},
"commentTaskLinkOnGitlabIssue": {
"type": "boolean",
"description": "When on, Kaneo comments a link back to the task on each issue opened in GitLab. Issues Kaneo creates from tasks are not commented on."
},
"isActive": {
"type": [
"boolean",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"baseUrl",
"projectPath",
"tokenType",
"maskedAccessToken",
"isActive",
"createdAt",
"updatedAt"
]
},
"GitlabDeleteResult": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"message": {
"type": "string"
}
},
"required": [
"success",
"message"
]
},
"GitlabImportResult": {
"type": "object",
"properties": {
"imported": {
"type": "number"
},
"updated": {
"type": "number",
"description": "Existing tasks that were refreshed from their issue."
},
"skipped": {
"type": "number"
},
"errors": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"imported",
"updated",
"skipped"
]
},
"GenericWebhookIntegration": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"webhookConfigured": {
"type": "boolean"
},
"maskedWebhookUrl": {
"type": [
"string",
"null"
]
},
"secretConfigured": {
"type": "boolean"
},
"maskedSecret": {
"type": [
"string",
"null"
],
"description": "A preview of the HMAC signing secret used to sign outgoing deliveries."
},
"events": {
"$ref": "#/components/schemas/GenericWebhookEvents"
},
"dueDateReminderLeadTimeMinutes": {
"type": "number",
"description": "How long before a due date the dueDateReminder event fires, in minutes."
},
"isActive": {
"type": [
"boolean",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"webhookConfigured",
"maskedWebhookUrl",
"secretConfigured",
"maskedSecret",
"events",
"dueDateReminderLeadTimeMinutes",
"isActive",
"createdAt",
"updatedAt"
]
},
"GenericWebhookEvents": {
"allOf": [
{
"$ref": "#/components/schemas/IntegrationEvents"
},
{
"type": "object",
"properties": {
"taskDeleted": {
"type": "boolean"
},
"taskMoved": {
"type": "boolean"
},
"taskDueDateChanged": {
"type": "boolean"
},
"taskAssigneeChanged": {
"type": "boolean"
},
"taskUnassigned": {
"type": "boolean"
},
"dueDateReminder": {
"type": "boolean"
}
},
"required": [
"taskDeleted",
"taskMoved",
"taskDueDateChanged",
"taskAssigneeChanged",
"taskUnassigned",
"dueDateReminder"
]
}
]
},
"IntegrationEvents": {
"type": "object",
"properties": {
"taskCreated": {
"type": "boolean"
},
"taskStatusChanged": {
"type": "boolean"
},
"taskPriorityChanged": {
"type": "boolean"
},
"taskTitleChanged": {
"type": "boolean"
},
"taskDescriptionChanged": {
"type": "boolean"
},
"taskCommentCreated": {
"type": "boolean"
}
},
"required": [
"taskCreated",
"taskStatusChanged",
"taskPriorityChanged",
"taskTitleChanged",
"taskDescriptionChanged",
"taskCommentCreated"
]
},
"DiscordIntegration": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"channelName": {
"type": [
"string",
"null"
]
},
"webhookConfigured": {
"type": "boolean"
},
"maskedWebhookUrl": {
"type": "string",
"description": "The webhook URL with its secret path segment masked, or an empty string when none is set."
},
"events": {
"$ref": "#/components/schemas/IntegrationEvents"
},
"isActive": {
"type": [
"boolean",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"channelName",
"webhookConfigured",
"maskedWebhookUrl",
"events",
"isActive",
"createdAt",
"updatedAt"
]
},
"MattermostIntegration": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"channelName": {
"type": [
"string",
"null"
]
},
"webhookConfigured": {
"type": "boolean"
},
"maskedWebhookUrl": {
"type": "string",
"description": "The webhook URL with its secret path segment masked, or an empty string when none is set."
},
"events": {
"$ref": "#/components/schemas/IntegrationEvents"
},
"isActive": {
"type": [
"boolean",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"channelName",
"webhookConfigured",
"maskedWebhookUrl",
"events",
"isActive",
"createdAt",
"updatedAt"
]
},
"SlackIntegration": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"channelName": {
"type": [
"string",
"null"
]
},
"webhookConfigured": {
"type": "boolean"
},
"maskedWebhookUrl": {
"type": "string",
"description": "The webhook URL with its secret path segment masked, or an empty string when none is set."
},
"events": {
"$ref": "#/components/schemas/IntegrationEvents"
},
"isActive": {
"type": [
"boolean",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"channelName",
"webhookConfigured",
"maskedWebhookUrl",
"events",
"isActive",
"createdAt",
"updatedAt"
]
},
"TelegramIntegration": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"chatId": {
"type": "string",
"description": "The Telegram chat, group, or channel messages are sent to."
},
"threadId": {
"type": [
"number",
"null"
],
"description": "Forum topic id, when the target group uses topics."
},
"chatLabel": {
"type": [
"string",
"null"
],
"description": "A human-readable name for the chat, shown in the UI."
},
"botTokenConfigured": {
"type": "boolean"
},
"maskedBotToken": {
"type": "string"
},
"events": {
"$ref": "#/components/schemas/IntegrationEvents"
},
"isActive": {
"type": [
"boolean",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"chatId",
"threadId",
"chatLabel",
"botTokenConfigured",
"maskedBotToken",
"events",
"isActive",
"createdAt",
"updatedAt"
]
},
"TaskRelationWithTasks": {
"allOf": [
{
"$ref": "#/components/schemas/TaskRelation"
},
{
"type": "object",
"properties": {
"sourceTask": {
"$ref": "#/components/schemas/RelatedTask"
},
"targetTask": {
"$ref": "#/components/schemas/RelatedTask"
}
},
"required": [
"sourceTask",
"targetTask"
]
}
]
},
"RelatedTask": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"status": {
"type": "string"
},
"isCompleted": {
"type": "boolean",
"description": "Whether the task is in a final column of its own project."
},
"priority": {
"type": [
"string",
"null"
]
},
"number": {
"type": [
"number",
"null"
]
},
"projectId": {
"type": "string"
},
"userId": {
"type": [
"string",
"null"
]
},
"assigneeName": {
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"title",
"status",
"isCompleted",
"priority",
"number",
"projectId",
"userId",
"assigneeName"
]
},
"TaskRelation": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"sourceTaskId": {
"type": "string"
},
"targetTaskId": {
"type": "string"
},
"relationType": {
"type": "string",
"description": "How the two tasks relate: subtask, blocks, or related."
},
"createdAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"sourceTaskId",
"targetTaskId",
"relationType",
"createdAt"
]
},
"CreatedExternalLink": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"taskId": {
"type": "string"
},
"integrationId": {
"type": [
"string",
"null"
]
},
"resourceType": {
"type": "string",
"description": "The resource kind: url for a manual link, or an integration resource such as issue or pull_request."
},
"externalId": {
"type": "string",
"description": "The provider's resource identifier, or the URL for a manual link."
},
"url": {
"type": "string"
},
"title": {
"type": [
"string",
"null"
]
},
"metadata": {
"description": "Provider-specific payload, parsed from the stored JSON string. Null when the link has no metadata."
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"taskId",
"integrationId",
"resourceType",
"externalId",
"url",
"title",
"createdAt",
"updatedAt"
]
},
"DeletedExternalLink": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
]
},
"ExternalLink": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"taskId": {
"type": "string"
},
"integrationId": {
"type": [
"string",
"null"
]
},
"resourceType": {
"type": "string",
"description": "The resource kind: url for a manual link, or an integration resource such as issue or pull_request."
},
"externalId": {
"type": "string",
"description": "The provider's resource identifier, or the URL for a manual link."
},
"url": {
"type": "string"
},
"title": {
"type": [
"string",
"null"
]
},
"metadata": {
"description": "Provider-specific payload, parsed from the stored JSON string. Null when the link has no metadata."
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"integration": {
"$ref": "#/components/schemas/ExternalLinkIntegration"
}
},
"required": [
"id",
"taskId",
"integrationId",
"resourceType",
"externalId",
"url",
"title",
"createdAt",
"updatedAt",
"integration"
]
},
"ExternalLinkIntegration": {
"type": [
"object",
"null"
],
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"id",
"type"
]
},
"WorkflowRule": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"integrationType": {
"type": "string",
"description": "The integration that emits the event, e.g. github."
},
"eventType": {
"type": "string",
"description": "The Kaneo event that triggers the move: branch_push, pr_opened, pr_merged, issue_opened, issue_closed or issue_reopened."
},
"columnId": {
"type": "string",
"description": "The column tasks are moved to when the event fires."
},
"columnName": {
"type": [
"string",
"null"
]
},
"columnSlug": {
"type": [
"string",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"integrationType",
"eventType",
"columnId",
"columnName",
"columnSlug",
"createdAt",
"updatedAt"
]
},
"WorkflowRuleRow": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"integrationType": {
"type": "string"
},
"eventType": {
"type": "string"
},
"columnId": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"integrationType",
"eventType",
"columnId",
"createdAt",
"updatedAt"
]
},
"PendingInvitation": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"email": {
"type": "string"
},
"workspaceId": {
"type": "string"
},
"workspaceName": {
"type": "string"
},
"inviterName": {
"type": "string"
},
"expiresAt": {
"type": "string",
"format": "date-time"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string",
"description": "Always pending here; expired and accepted ones are filtered out."
}
},
"required": [
"id",
"email",
"workspaceId",
"workspaceName",
"inviterName",
"expiresAt",
"createdAt",
"status"
]
},
"InvitationDetails": {
"type": "object",
"properties": {
"valid": {
"type": "boolean",
"description": "True only when the invitation can still be accepted."
},
"invitation": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"email": {
"type": "string"
},
"workspaceName": {
"type": "string"
},
"inviterName": {
"type": "string"
},
"expiresAt": {
"type": "string",
"format": "date-time"
},
"status": {
"type": "string"
},
"expired": {
"type": "boolean"
}
},
"required": [
"id",
"email",
"workspaceName",
"inviterName",
"expiresAt",
"status",
"expired"
],
"description": "Omitted when the invitation does not exist, was already accepted, or was canceled -- the details are withheld rather than leaked."
},
"error": {
"type": "string",
"description": "Why the invitation is unusable, when valid is false."
}
},
"required": [
"valid"
]
},
"Workspace": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"slug": {
"type": "string"
},
"logo": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"role": {
"type": [
"string",
"null"
],
"description": "The caller's workspace role: a built-in role (owner, admin, member, viewer) or the name of a custom role defined in the workspace. Several roles are comma-separated, for example owner,viewer. Null when an instance admin views a workspace they are not a member of."
}
},
"required": [
"id",
"name",
"slug",
"logo",
"description",
"createdAt",
"role"
]
},
"WorkspaceMember": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"image": {
"type": [
"string",
"null"
]
},
"role": {
"type": "string",
"description": "The member's workspace role: a built-in role (owner, admin, member, viewer) or the name of a custom role defined in the workspace. Several roles are comma-separated, for example owner,viewer."
}
},
"required": [
"id",
"name",
"email",
"image",
"role"
]
},
"MemberProjectAccess": {
"type": "object",
"properties": {
"userId": {
"type": "string"
},
"projectAccess": {
"type": "string",
"enum": [
"all",
"selected"
]
},
"projectIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"userId",
"projectAccess",
"projectIds"
]
},
"WorkspaceCapabilities": {
"type": "object",
"properties": {
"manageProjects": {
"type": "boolean"
},
"createProjects": {
"type": "boolean"
},
"updateProjects": {
"type": "boolean"
},
"shareProjects": {
"type": "boolean"
},
"deleteProjects": {
"type": "boolean"
},
"updateTasks": {
"type": "boolean"
},
"createTasks": {
"type": "boolean"
},
"deleteTasks": {
"type": "boolean"
},
"assignTasks": {
"type": "boolean"
},
"createLabels": {
"type": "boolean"
},
"updateLabels": {
"type": "boolean"
},
"deleteLabels": {
"type": "boolean"
},
"manageWorkspace": {
"type": "boolean"
},
"manageSettings": {
"type": "boolean"
},
"deleteWorkspace": {
"type": "boolean"
},
"inviteUsers": {
"type": "boolean"
},
"manageTeam": {
"type": "boolean"
},
"updateMembers": {
"type": "boolean"
},
"removeMembers": {
"type": "boolean"
}
},
"required": [
"manageProjects",
"createProjects",
"updateProjects",
"shareProjects",
"deleteProjects",
"updateTasks",
"createTasks",
"deleteTasks",
"assignTasks",
"createLabels",
"updateLabels",
"deleteLabels",
"manageWorkspace",
"manageSettings",
"deleteWorkspace",
"inviteUsers",
"manageTeam",
"updateMembers",
"removeMembers"
],
"description": "Whether the caller's roles (and API key scope, if any) allow each named action in the workspace. Mirrors the checks the API enforces."
},
"CustomFieldDefinition": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"projectId": {
"type": "string"
},
"name": {
"type": "string"
},
"type": {
"type": "string"
},
"required": {
"type": "boolean"
},
"defaultValue": {
"type": [
"string",
"null"
]
},
"options": {},
"position": {
"type": "number"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"projectId",
"name",
"type",
"required",
"defaultValue",
"position",
"createdAt",
"updatedAt"
]
},
"CustomFieldValue": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"taskId": {
"type": "string"
},
"fieldId": {
"type": "string"
},
"value": {
"type": [
"string",
"null"
]
},
"fieldName": {
"type": "string"
},
"fieldPosition": {
"type": "number"
},
"fieldType": {
"type": "string"
},
"fieldOptions": {}
},
"required": [
"id",
"taskId",
"fieldId",
"value",
"fieldName",
"fieldPosition",
"fieldType"
]
},
"CustomFieldFilterValues": {
"type": "object",
"properties": {
"fieldId": {
"type": "string"
},
"fieldName": {
"type": "string"
},
"fieldType": {
"type": "string"
},
"values": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"fieldId",
"fieldName",
"fieldType",
"values"
]
},
"ReorderCustomFieldsResponse": {},
"SetCustomFieldValueResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"taskId": {
"type": "string"
},
"fieldId": {
"type": "string"
},
"value": {
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"taskId",
"fieldId",
"value"
]
},
"ImportSourceProjectList": {
"type": "object",
"properties": {
"projects": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ImportSourceProject"
}
}
},
"required": [
"projects"
]
},
"ImportSourceProject": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"detail": {
"type": "string",
"description": "Extra context shown next to the name, such as the source workspace or team."
}
},
"required": [
"id",
"name"
]
},
"ImportConnection": {
"oneOf": [
{
"$ref": "#/components/schemas/PlankaImportConnection"
},
{
"$ref": "#/components/schemas/PlaneImportConnection"
},
{
"$ref": "#/components/schemas/LinearImportConnection"
},
{
"$ref": "#/components/schemas/JiraImportConnection"
},
{
"$ref": "#/components/schemas/TrelloImportConnection"
},
{
"$ref": "#/components/schemas/KaneoImportConnection"
}
],
"discriminator": {
"propertyName": "source",
"mapping": {
"planka": "#/components/schemas/PlankaImportConnection",
"plane": "#/components/schemas/PlaneImportConnection",
"linear": "#/components/schemas/LinearImportConnection",
"jira": "#/components/schemas/JiraImportConnection",
"trello": "#/components/schemas/TrelloImportConnection",
"kaneo": "#/components/schemas/KaneoImportConnection"
}
},
"description": "Where to import from and how to sign in. Credentials are used only for this request and the import it starts; they are never stored, logged, or returned."
},
"PlankaImportConnection": {
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": [
"planka"
]
},
"url": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"description": "Address of the PLANKA instance.",
"example": "https://planka.example.com"
},
"apiKey": {
"type": "string",
"minLength": 1,
"maxLength": 4096,
"description": "A PLANKA API key. Send it, or username and password. Used only for this import and never stored or logged."
},
"username": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "Username or email of a PLANKA account, used with password when no API key is sent."
},
"password": {
"type": "string",
"minLength": 1,
"maxLength": 4096,
"description": "Password of that PLANKA account. Used only for this import and never stored or logged."
}
},
"required": [
"source",
"url"
]
},
"PlaneImportConnection": {
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": [
"plane"
]
},
"url": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"description": "Address of the Plane instance, e.g. https://app.plane.so for Plane Cloud.",
"example": "https://planka.example.com"
},
"apiKey": {
"type": "string",
"minLength": 1,
"maxLength": 4096,
"description": "A Plane personal access token. Used only for this import and never stored or logged."
},
"workspaceSlug": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "Slug of the Plane workspace to read. Optional when the URL already names it."
}
},
"required": [
"source",
"url",
"apiKey"
]
},
"LinearImportConnection": {
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": [
"linear"
]
},
"apiKey": {
"type": "string",
"minLength": 1,
"maxLength": 4096,
"description": "A Linear personal API key. Used only for this import and never stored or logged."
}
},
"required": [
"source",
"apiKey"
]
},
"JiraImportConnection": {
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": [
"jira"
]
},
"url": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"description": "Address of the Jira site, e.g. https://your-site.atlassian.net for Jira Cloud, or your own domain for Jira Data Center.",
"example": "https://your-site.atlassian.net"
},
"email": {
"type": "string",
"minLength": 1,
"maxLength": 320,
"description": "Email of the Atlassian account the API token belongs to. Required for Jira Cloud; leave it out for Jira Data Center."
},
"apiToken": {
"type": "string",
"minLength": 1,
"maxLength": 4096,
"description": "A Jira Cloud API token, sent with email, or a Jira Data Center personal access token, sent without email. Used only for this import and never stored or logged."
}
},
"required": [
"source",
"url",
"apiToken"
]
},
"TrelloImportConnection": {
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": [
"trello"
]
},
"token": {
"type": "string",
"minLength": 1,
"maxLength": 4096,
"description": "A Trello token authorized for this Kaneo instance. Used only for this import and never stored or logged."
},
"apiKey": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "Trello API key, a public app identifier. Defaults to the server's TRELLO_API_KEY."
}
},
"required": [
"source",
"token"
]
},
"KaneoImportConnection": {
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": [
"kaneo"
]
},
"url": {
"type": "string",
"minLength": 1,
"maxLength": 2048,
"description": "Address of the Kaneo instance to import from.",
"example": "https://planka.example.com"
},
"apiKey": {
"type": "string",
"minLength": 1,
"maxLength": 4096,
"description": "An API key from that Kaneo instance. Used only for this import and never stored or logged."
}
},
"required": [
"source",
"url",
"apiKey"
]
},
"ImportRunEnvelope": {
"type": "object",
"properties": {
"run": {
"$ref": "#/components/schemas/ImportRun"
}
},
"required": [
"run"
]
},
"ImportRun": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"workspaceId": {
"type": "string"
},
"source": {
"type": "string",
"enum": [
"planka",
"plane",
"linear",
"jira",
"trello",
"kaneo"
]
},
"status": {
"type": "string",
"enum": [
"running",
"completed",
"failed"
],
"description": "completed once every selected project was attempted, even if some failed; check each project's status. failed means the run itself stopped."
},
"includeComments": {
"type": "boolean"
},
"message": {
"type": [
"string",
"null"
],
"description": "Latest progress line while the run is running."
},
"error": {
"type": [
"string",
"null"
]
},
"projects": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ImportRunProject"
}
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"finishedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
"required": [
"id",
"workspaceId",
"source",
"status",
"includeComments",
"message",
"error",
"projects",
"createdAt",
"finishedAt"
]
},
"ImportRunProject": {
"type": "object",
"properties": {
"sourceId": {
"type": "string"
},
"name": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"pending",
"running",
"completed",
"failed"
]
},
"projectId": {
"type": [
"string",
"null"
],
"description": "The Kaneo project created by the import, once completed."
},
"projectSlug": {
"type": [
"string",
"null"
]
},
"counts": {
"$ref": "#/components/schemas/ImportCounts"
},
"warnings": {
"type": "array",
"items": {
"type": "string"
}
},
"error": {
"type": [
"string",
"null"
]
}
},
"required": [
"sourceId",
"name",
"status",
"projectId",
"projectSlug",
"counts",
"warnings",
"error"
]
},
"ImportCounts": {
"type": "object",
"properties": {
"columns": {
"type": "integer"
},
"tasks": {
"type": "integer"
},
"labels": {
"type": "integer"
},
"comments": {
"type": "integer"
},
"assignees": {
"type": "integer",
"description": "Tasks that were assigned to a workspace member."
},
"relations": {
"type": "integer"
}
},
"required": [
"columns",
"tasks",
"labels",
"comments",
"assignees",
"relations"
]
},
"ImportRunList": {
"type": "object",
"properties": {
"runs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ImportRun"
}
}
},
"required": [
"runs"
]
},
"TrelloAuthorizeUrl": {
"type": "object",
"properties": {
"authorizeUrl": {
"type": [
"string",
"null"
],
"description": "Trello page where the user approves read access for Kaneo and copies a token. null when the server has no TRELLO_API_KEY; the user then sends their own Trello API key with the token."
}
},
"required": [
"authorizeUrl"
]
},
"CurrentUser": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"image": {
"type": [
"string",
"null"
]
},
"role": {
"type": [
"string",
"null"
]
}
},
"required": [
"id",
"name",
"email",
"image",
"role"
]
},
"UserAvatar": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"url": {
"type": "string",
"description": "Public URL for the stored avatar, served from /api/user/avatar/{id}."
},
"size": {
"type": "number",
"description": "Decoded size in bytes."
}
},
"required": [
"id",
"url",
"size"
]
},
"UserAvatarDeleted": {
"type": "object",
"properties": {
"deleted": {
"type": "boolean",
"description": "False when the user had no uploaded avatar to remove."
}
},
"required": [
"deleted"
]
},
"AdminUserList": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AdminUser"
}
},
"total": {
"type": "integer",
"description": "Number of users matching the search, across all pages."
}
},
"required": [
"users",
"total"
]
},
"AdminUser": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"emailVerified": {
"type": "boolean"
},
"image": {
"type": [
"string",
"null"
]
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"role": {
"type": [
"string",
"null"
]
},
"banned": {
"type": "boolean"
},
"banReason": {
"type": [
"string",
"null"
]
},
"banExpires": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
"required": [
"id",
"name",
"email",
"emailVerified",
"image",
"createdAt",
"updatedAt",
"role",
"banned",
"banReason",
"banExpires"
]
},
"AdminWorkspaceList": {
"type": "object",
"properties": {
"workspaces": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AdminWorkspace"
}
},
"total": {
"type": "integer",
"description": "Number of workspaces matching the search, across all pages."
}
},
"required": [
"workspaces",
"total"
]
},
"AdminWorkspace": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"slug": {
"type": "string"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"memberCount": {
"type": "integer"
},
"projectCount": {
"type": "integer"
},
"owners": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"type": "string"
}
},
"required": [
"id",
"name",
"email"
]
}
}
},
"required": [
"id",
"name",
"slug",
"createdAt",
"memberCount",
"projectCount",
"owners"
]
},
"AdminWorkspaceMember": {
"type": "object",
"properties": {
"userId": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"image": {
"type": [
"string",
"null"
]
},
"role": {
"type": "string"
},
"joinedAt": {
"type": "string",
"format": "date-time"
}
},
"required": [
"userId",
"name",
"email",
"image",
"role",
"joinedAt"
]
}
},
"parameters": {}
},
"paths": {
"/instance/status": {
"get": {
"operationId": "getInstanceStatus",
"tags": [
"Instance"
],
"summary": "Get instance status",
"description": "Public instance setup status. When hasUsers is false the next signup becomes the instance admin.",
"security": [],
"responses": {
"200": {
"description": "Instance status",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InstanceStatus"
}
}
}
}
}
}
},
"/public-project/{id}": {
"get": {
"operationId": "getPublicProject",
"tags": [
"Projects"
],
"summary": "Get a public project board",
"description": "Read a public board in bounded task pages. Visibility is checked before loading tasks. Continue through pagination.totalPages for all tasks and through relatedPage/pagination.relatedTotalPages for their complete related records.",
"security": [],
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
},
{
"schema": {
"type": "string"
},
"required": false,
"name": "status",
"in": "query"
},
{
"schema": {
"type": "string"
},
"required": false,
"name": "priority",
"in": "query"
},
{
"schema": {
"type": "string"
},
"required": false,
"name": "assigneeId",
"in": "query"
},
{
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 1000000
},
"required": false,
"name": "page",
"in": "query"
},
{
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 1000000
},
"required": false,
"name": "relatedPage",
"in": "query"
},
{
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100
},
"required": false,
"name": "limit",
"in": "query"
},
{
"schema": {
"type": "string",
"enum": [
"createdAt",
"priority",
"dueDate",
"position",
"title",
"number"
]
},
"required": false,
"name": "sortBy",
"in": "query"
},
{
"schema": {
"type": "string",
"enum": [
"asc",
"desc"
]
},
"required": false,
"name": "sortOrder",
"in": "query"
},
{
"schema": {
"type": "string"
},
"required": false,
"name": "dueBefore",
"in": "query"
},
{
"schema": {
"type": "string"
},
"required": false,
"name": "dueAfter",
"in": "query"
}
],
"responses": {
"200": {
"description": "A public board page",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PublicBoardPage"
}
}
}
},
"400": {
"description": "Invalid pagination or filters",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "Project is not public",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"503": {
"description": "Task list request timed out",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/public-project/{id}/description": {
"get": {
"operationId": "getPublicProjectDescriptionPage",
"tags": [
"Projects"
],
"summary": "Read a public project description page",
"security": [],
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
},
{
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 2000000000,
"default": 0
},
"required": false,
"name": "offset",
"in": "query"
},
{
"schema": {
"type": "string",
"pattern": "^[0-9]{1,10}$"
},
"required": false,
"name": "version",
"in": "query"
}
],
"responses": {
"200": {
"description": "Public project description page",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskDescriptionPage"
}
}
}
},
"400": {
"description": "Invalid description cursor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Public project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Description changed or no longer public",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"503": {
"description": "Description request timed out",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/public-project/{id}/task/{taskId}/description": {
"get": {
"operationId": "getPublicTaskDescriptionPage",
"tags": [
"Projects"
],
"summary": "Read a public task description page",
"security": [],
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
},
{
"schema": {
"type": "string"
},
"required": true,
"name": "taskId",
"in": "path"
},
{
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 2000000000,
"default": 0
},
"required": false,
"name": "offset",
"in": "query"
},
{
"schema": {
"type": "string",
"pattern": "^[0-9]{1,10}$"
},
"required": false,
"name": "version",
"in": "query"
}
],
"responses": {
"200": {
"description": "Public task description page",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskDescriptionPage"
}
}
}
},
"400": {
"description": "Invalid description cursor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Public task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Description changed or no longer public",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"503": {
"description": "Description request timed out",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/calendar-feed/{token}/calendar.ics": {
"get": {
"operationId": "getCalendarFeed",
"tags": [
"Calendar feeds"
],
"summary": "Subscribe to a calendar feed",
"description": "Read scheduled project tasks using a secret calendar feed link. Anyone with the link can read matching task titles, descriptions, and dates. Responses are streamed; descriptions longer than 4096 characters and titles or calendar names longer than 1024 characters are truncated with an ellipsis.",
"security": [],
"parameters": [
{
"schema": {
"type": "string",
"pattern": "^[a-f0-9]{64}$"
},
"required": true,
"name": "token",
"in": "path"
}
],
"responses": {
"200": {
"description": "iCalendar feed",
"content": {
"text/calendar": {
"schema": {
"type": "string"
}
}
}
},
"400": {
"description": "Invalid feed token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Calendar feed not found or revoked",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/auth/get-session": {
"get": {
"operationId": "getSession",
"tags": [
"Authentication"
],
"summary": "Get session",
"description": "Get the current authenticated session, or null when the caller is not signed in. Served by Better Auth.",
"security": [],
"responses": {
"200": {
"description": "Current session details, or null when unauthenticated"
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/asset/{id}": {
"get": {
"operationId": "getAsset",
"tags": [
"Assets"
],
"summary": "Download asset",
"description": "Download an uploaded asset. Readable without signing in only when it belongs to a public project; image types are served inline, everything else as an attachment.",
"security": [],
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"200": {
"description": "The requested asset binary stream",
"content": {
"/": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"304": {
"description": "Not modified"
},
"403": {
"description": "Staged upload owned by another user, or no access to the asset's project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Asset not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/user/avatar/{id}": {
"get": {
"operationId": "getUserAvatar",
"tags": [
"User"
],
"summary": "Download avatar",
"description": "Download a user avatar by its avatar ID. Public, immutable, and cache-friendly: the id changes whenever the avatar is replaced.",
"security": [],
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"200": {
"description": "The avatar image",
"content": {
"image/": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"304": {
"description": "Not modified"
},
"404": {
"description": "Avatar not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/config": {
"get": {
"operationId": "getConfig",
"tags": [
"Config"
],
"summary": "Get config",
"description": "Public instance settings: which sign-in methods, registration paths, and features are enabled.",
"security": [],
"responses": {
"200": {
"description": "Application settings",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Config"
}
}
}
}
}
}
},
"/auth/organization/accept-invitation": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "acceptOrganizationInvitation",
"summary": "Accept Organization Invitation",
"description": "Accept an invitation to an organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"invitationId": {
"type": "string",
"description": "The ID of the invitation to accept"
}
},
"required": [
"invitationId"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"invitation": {
"type": "object",
"additionalProperties": {}
},
"member": {
"type": "object",
"additionalProperties": {}
}
}
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/add-team-member": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "addOrganizationTeamMember",
"summary": "Add Organization Team Member",
"description": "The newly created member",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"teamId": {
"type": "string",
"description": "The team the user should be a member of."
},
"userId": {
"type": "string",
"description": "The user Id which represents the user to be added as a member."
},
"organizationId": {
"type": "string",
"description": "The organization ID which the team falls under. If not provided, it will default to the user's active organization."
}
},
"required": [
"teamId",
"userId"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier of the team member"
},
"userId": {
"type": "string",
"description": "The user ID of the team member"
},
"teamId": {
"type": "string",
"description": "The team ID of the team the team member is in"
},
"createdAt": {
"type": "string",
"description": "Timestamp when the team member was created"
}
},
"required": [
"id",
"userId",
"teamId",
"createdAt"
]
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/cancel-invitation": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "cancelOrganizationInvitation",
"summary": "Cancel Organization Invitation",
"description": "Cancel Organization Invitation",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"invitationId": {
"type": "string",
"description": "The ID of the invitation to cancel"
}
},
"required": [
"invitationId"
]
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/check-slug": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "checkOrganizationSlug",
"summary": "Check Organization Slug",
"description": "Check Organization Slug",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"slug": {
"type": "string",
"description": "The organization slug to check. Eg: "my-org""
}
},
"required": [
"slug"
]
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/create": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "createOrganization",
"summary": "Create Organization",
"description": "Create an organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the organization"
},
"slug": {
"type": "string",
"description": "The slug of the organization"
},
"userId": {
"type": "string",
"description": "The user id of the organization creator. If not provided, the current user will be used. Should only be used by admins or when called by the server. server-only. Eg: "user-id""
},
"logo": {
"type": [
"string",
"null"
],
"description": "The logo of the organization"
},
"metadata": {
"type": "object",
"additionalProperties": {},
"description": "The metadata of the organization"
},
"keepCurrentActiveOrganization": {
"type": "boolean",
"description": "Whether to keep the current active organization active after creating a new one. Eg: true"
},
"description": {
"type": [
"string",
"null"
]
}
},
"required": [
"name",
"slug"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/create-role": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "createOrganizationRole",
"summary": "Create Organization Role",
"description": "Create Organization Role",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"organizationId": {
"type": "string"
},
"role": {
"type": "string",
"description": "The name of the role to create"
},
"permission": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"description": "The permission to assign to the role"
},
"additionalFields": {
"type": "object",
"additionalProperties": {}
}
},
"required": [
"role",
"permission"
]
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/create-team": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "createOrganizationTeam",
"summary": "Create Organization Team",
"description": "Create a new team within an organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the team. Eg: "my-team""
},
"organizationId": {
"type": "string",
"description": "The organization ID which the team will be created in. Defaults to the active organization. Eg: "organization-id""
}
},
"required": [
"name"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier of the created team"
},
"name": {
"type": "string",
"description": "Name of the team"
},
"organizationId": {
"type": "string",
"description": "ID of the organization the team belongs to"
},
"createdAt": {
"type": "string",
"description": "Timestamp when the team was created"
},
"updatedAt": {
"type": "string",
"description": "Timestamp when the team was last updated"
}
},
"required": [
"id",
"name",
"organizationId",
"createdAt",
"updatedAt"
]
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/delete": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "deleteOrganization",
"summary": "Delete Organization",
"description": "Delete an organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"organizationId": {
"type": "string",
"description": "The organization id to delete"
}
},
"required": [
"organizationId"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/delete-role": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "deleteOrganizationRole",
"summary": "Delete Organization Role",
"description": "Delete Organization Role",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"allOf": [
{
"type": "object",
"properties": {
"organizationId": {
"type": "string"
}
}
},
{
"anyOf": [
{
"type": "object",
"properties": {
"roleName": {
"type": "string",
"description": "The name of the role to delete"
}
},
"required": [
"roleName"
]
},
{
"type": "object",
"properties": {
"roleId": {
"type": "string",
"description": "The id of the role to delete"
}
},
"required": [
"roleId"
]
}
]
}
]
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/get-active-member": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "getOrganizationActiveMember",
"summary": "Get Organization Active Member",
"description": "Get the member details of the active organization",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"userId": {
"type": "string"
},
"organizationId": {
"type": "string"
},
"role": {
"type": "string"
}
},
"required": [
"id",
"userId",
"organizationId",
"role"
]
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/get-active-member-role": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "getOrganizationActiveMemberRole",
"summary": "Get Organization Active Member Role",
"description": "Get Organization Active Member Role",
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/get-full-organization": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "getOrganizationFullOrganization",
"summary": "Get Organization Full Organization",
"description": "Get the full organization",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/get-invitation": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "getOrganizationInvitation",
"summary": "Get Organization Invitation",
"description": "Get an invitation by ID",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "query"
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"email": {
"type": "string"
},
"role": {
"type": "string"
},
"organizationId": {
"type": "string"
},
"inviterId": {
"type": "string"
},
"status": {
"type": "string"
},
"expiresAt": {
"type": "string"
},
"organizationName": {
"type": "string"
},
"organizationSlug": {
"type": "string"
},
"inviterEmail": {
"type": "string"
},
"projectAccess": {
"type": "string",
"enum": [
"all",
"selected"
],
"description": ""all" gives access to every project once accepted; "selected" limits the member to projectIds."
},
"projectIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Projects the invitation grants when projectAccess is "selected". Only projects the viewer can access are listed."
}
},
"required": [
"id",
"email",
"role",
"organizationId",
"inviterId",
"status",
"expiresAt",
"organizationName",
"organizationSlug",
"inviterEmail",
"projectAccess",
"projectIds"
]
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/get-role": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "getOrganizationRole",
"summary": "Get Organization Role",
"description": "Get Organization Role",
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/has-permission": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "hasOrganizationPermission",
"summary": "Check Organization Permission",
"description": "Check if the user has permission",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"permission": {
"type": "object",
"additionalProperties": {},
"description": "The permission to check"
},
"permissions": {
"type": "object",
"additionalProperties": {},
"description": "The permission to check"
}
},
"required": [
"permissions"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"success": {
"type": "boolean"
}
},
"required": [
"success"
]
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/invite-member": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "inviteOrganizationMember",
"summary": "Invite Organization Member",
"description": "Create or resend an invitation. Email delivery failures return 502; the invitation remains pending and can be resent after correcting SMTP settings.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "The email address of the user to invite"
},
"role": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "The role(s) to assign to the user. It can be admin, member, owner. Eg: "member""
},
"organizationId": {
"type": "string",
"description": "The organization ID to invite the user to"
},
"resend": {
"type": "boolean",
"description": "Resend the invitation email, if the user is already invited. Eg: true"
},
"teamId": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"projectAccess": {
"type": "string",
"enum": [
"all",
"selected"
],
"description": ""all" (the default) gives access to every project. "selected" limits the new member to projectIds once they accept."
},
"projectIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Projects the new member can access when projectAccess is "selected". They must belong to the workspace, and an inviter whose own access is limited can only choose projects they can access."
}
},
"required": [
"email",
"role"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"email": {
"type": "string"
},
"role": {
"type": "string"
},
"organizationId": {
"type": "string"
},
"inviterId": {
"type": "string"
},
"status": {
"type": "string"
},
"expiresAt": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"projectAccess": {
"type": "string",
"enum": [
"all",
"selected"
],
"description": ""all" gives access to every project once accepted; "selected" limits the member to projectIds."
},
"projectIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Projects the invitation grants when projectAccess is "selected". Only projects the viewer can access are listed."
}
},
"required": [
"id",
"email",
"role",
"organizationId",
"inviterId",
"status",
"expiresAt",
"createdAt",
"projectAccess",
"projectIds"
]
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
},
"502": {
"description": "Invitation saved, but email delivery failed",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"code": {
"type": "string",
"enum": [
"INVITATION_EMAIL_FAILED"
]
},
"message": {
"type": "string"
}
},
"required": [
"code",
"message"
]
}
}
}
}
}
}
},
"/auth/organization/leave": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "leaveOrganization",
"summary": "Leave Organization",
"description": "Leave Organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"organizationId": {
"type": "string",
"description": "The organization Id for the member to leave. Eg: "organization-id""
}
},
"required": [
"organizationId"
]
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/list": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "listOrganization",
"summary": "List Organization",
"description": "List all organizations",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {}
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/list-invitations": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "listOrganizationInvitations",
"summary": "List Organization Invitations",
"description": "List Organization Invitations",
"parameters": [
{
"schema": {
"type": "string"
},
"required": false,
"name": "organizationId",
"in": "query"
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"email": {
"type": "string"
},
"role": {
"type": "string"
},
"organizationId": {
"type": "string"
},
"inviterId": {
"type": "string"
},
"status": {
"type": "string"
},
"expiresAt": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"projectAccess": {
"type": "string",
"enum": [
"all",
"selected"
],
"description": ""all" gives access to every project once accepted; "selected" limits the member to projectIds."
},
"projectIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "Projects the invitation grants when projectAccess is "selected". Only projects the viewer can access are listed."
}
},
"required": [
"id",
"email",
"role",
"organizationId",
"inviterId",
"status",
"expiresAt",
"createdAt",
"projectAccess",
"projectIds"
]
}
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/list-members": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "listOrganizationMembers",
"summary": "List Organization Members",
"description": "List Organization Members",
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/list-roles": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "listOrganizationRoles",
"summary": "List Organization Roles",
"description": "List Organization Roles",
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/list-team-members": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "listOrganizationTeamMembers",
"summary": "List Organization Team Members",
"description": "List the members of the given team.",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier of the team member"
},
"userId": {
"type": "string",
"description": "The user ID of the team member"
},
"teamId": {
"type": "string",
"description": "The team ID of the team the team member is in"
},
"createdAt": {
"type": "string",
"description": "Timestamp when the team member was created"
}
},
"required": [
"id",
"userId",
"teamId",
"createdAt"
]
}
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/list-teams": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "listOrganizationTeams",
"summary": "List Organization Teams",
"description": "List all teams in an organization",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier of the team"
},
"name": {
"type": "string",
"description": "Name of the team"
},
"organizationId": {
"type": "string",
"description": "ID of the organization the team belongs to"
},
"createdAt": {
"type": "string",
"description": "Timestamp when the team was created"
},
"updatedAt": {
"type": "string",
"description": "Timestamp when the team was last updated"
}
},
"required": [
"id",
"name",
"organizationId",
"createdAt",
"updatedAt"
]
}
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/list-user-invitations": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "listOrganizationUserInvitations",
"summary": "List Organization User Invitations",
"description": "List all invitations a user has received",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"email": {
"type": "string"
},
"role": {
"type": "string"
},
"organizationId": {
"type": "string"
},
"organizationName": {
"type": "string"
},
"inviterId": {
"type": "string",
"description": "The ID of the user who created the invitation"
},
"teamId": {
"type": [
"string",
"null"
],
"description": "The ID of the team associated with the invitation"
},
"status": {
"type": "string"
},
"expiresAt": {
"type": "string"
},
"createdAt": {
"type": "string"
}
},
"required": [
"id",
"email",
"role",
"organizationId",
"organizationName",
"inviterId",
"status",
"expiresAt",
"createdAt"
]
}
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/list-user-teams": {
"get": {
"tags": [
"Organization Management"
],
"operationId": "listOrganizationUserTeams",
"summary": "List Organization User Teams",
"description": "List all teams that the current user is a part of.",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {}
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/reject-invitation": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "rejectOrganizationInvitation",
"summary": "Reject Organization Invitation",
"description": "Reject an invitation to an organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"invitationId": {
"type": "string",
"description": "The ID of the invitation to reject"
}
},
"required": [
"invitationId"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"invitation": {
"type": "object",
"additionalProperties": {}
},
"member": {
"type": [
"object",
"null"
],
"additionalProperties": {}
}
}
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/remove-member": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "removeOrganizationMember",
"summary": "Remove Organization Member",
"description": "Remove a member from an organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"memberIdOrEmail": {
"type": "string",
"description": "The ID or email of the member to remove"
},
"organizationId": {
"type": "string",
"description": "The ID of the organization to remove the member from. If not provided, the active organization will be used. Eg: "org-id""
}
},
"required": [
"memberIdOrEmail"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"member": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"userId": {
"type": "string"
},
"organizationId": {
"type": "string"
},
"role": {
"type": "string"
}
},
"required": [
"id",
"userId",
"organizationId",
"role"
]
}
},
"required": [
"member"
]
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/remove-team": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "removeOrganizationTeam",
"summary": "Remove Organization Team",
"description": "Remove a team from an organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"teamId": {
"type": "string",
"description": "The team ID of the team to remove. Eg: "team-id""
},
"organizationId": {
"type": "string",
"description": "The organization ID which the team falls under. If not provided, it will default to the user's active organization. Eg: "organization-id""
}
},
"required": [
"teamId"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"enum": [
"Team removed successfully."
],
"description": "Confirmation message indicating successful removal"
}
},
"required": [
"message"
]
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/remove-team-member": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "removeOrganizationTeamMember",
"summary": "Remove Organization Team Member",
"description": "Remove a member from a team",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"teamId": {
"type": "string",
"description": "The team the user should be removed from."
},
"userId": {
"type": "string",
"description": "The user which should be removed from the team."
},
"organizationId": {
"type": "string",
"description": "The organization ID which the team falls under. If not provided, it will default to the user's active organization."
}
},
"required": [
"teamId",
"userId"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"enum": [
"Team member removed successfully."
],
"description": "Confirmation message indicating successful removal"
}
},
"required": [
"message"
]
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/set-active": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "setOrganizationActive",
"summary": "Set Organization Active",
"description": "Set the active organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"organizationId": {
"type": [
"string",
"null"
],
"description": "The organization id to set as active. It can be null to unset the active organization. Eg: "org-id""
},
"organizationSlug": {
"type": "string",
"description": "The organization slug to set as active. It can be null to unset the active organization if organizationId is not provided. Eg: "org-slug""
}
}
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/set-active-team": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "setOrganizationActiveTeam",
"summary": "Set Organization Active Team",
"description": "Set the active team for the current active organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"teamId": {
"type": [
"string",
"null"
],
"description": "The team id to set as active. It can be null to unset the active team"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/update": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "updateOrganization",
"summary": "Update Organization",
"description": "Update an organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string",
"description": "The name of the organization"
},
"slug": {
"type": "string",
"description": "The slug of the organization"
},
"logo": {
"type": [
"string",
"null"
],
"description": "The logo of the organization"
},
"metadata": {
"type": "object",
"additionalProperties": {},
"description": "The metadata of the organization"
}
}
},
"organizationId": {
"type": "string",
"description": "The organization ID. Eg: "org-id""
}
},
"required": [
"data"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/update-member-role": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "updateOrganizationMemberRole",
"summary": "Update Organization Member Role",
"description": "Update the role of a member in an organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"role": {
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "The new role to be applied. This can be a string or array of strings representing the roles. Eg: ["admin", "sale"]"
},
"memberId": {
"type": "string",
"description": "The member id to apply the role update to. Eg: "member-id""
},
"organizationId": {
"type": "string",
"description": "An optional organization ID which the member is a part of to apply the role update. If not provided, you must provide session headers to get the active organization. Eg: "organization-id""
}
},
"required": [
"role",
"memberId"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"member": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"userId": {
"type": "string"
},
"organizationId": {
"type": "string"
},
"role": {
"type": "string"
}
},
"required": [
"id",
"userId",
"organizationId",
"role"
]
}
},
"required": [
"member"
]
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/update-role": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "updateOrganizationRole",
"summary": "Update Organization Role",
"description": "Update Organization Role",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"allOf": [
{
"type": "object",
"properties": {
"organizationId": {
"type": "string"
},
"data": {
"type": "object",
"properties": {
"permission": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"roleName": {
"type": "string"
}
}
}
},
"required": [
"data"
]
},
{
"anyOf": [
{
"type": "object",
"properties": {
"roleName": {
"type": "string",
"description": "The name of the role to update"
}
},
"required": [
"roleName"
]
},
{
"type": "object",
"properties": {
"roleId": {
"type": "string",
"description": "The id of the role to update"
}
},
"required": [
"roleId"
]
}
]
}
]
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/organization/update-team": {
"post": {
"tags": [
"Organization Management"
],
"operationId": "updateOrganizationTeam",
"summary": "Update Organization Team",
"description": "Update an existing team in an organization",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"teamId": {
"type": "string",
"description": "The ID of the team to be updated. Eg: "team-id""
},
"data": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"organizationId": {
"type": "string"
},
"createdAt": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
}
},
"required": [
"teamId",
"data"
]
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier of the updated team"
},
"name": {
"type": "string",
"description": "Updated name of the team"
},
"organizationId": {
"type": "string",
"description": "ID of the organization the team belongs to"
},
"createdAt": {
"type": "string",
"description": "Timestamp when the team was created"
},
"updatedAt": {
"type": "string",
"description": "Timestamp when the team was last updated"
}
},
"required": [
"id",
"name",
"organizationId",
"createdAt",
"updatedAt"
]
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/auth/device": {
"get": {
"operationId": "getDeviceAuthorizationPage",
"tags": [
"Authentication"
],
"summary": "Device authorization page",
"description": "Better Auth serves this as JSON. A top-level browser navigation is redirected to the web app's device screen instead, so opening the URL by hand shows a page rather than a JSON blob.",
"security": [],
"parameters": [
{
"schema": {
"type": "string",
"description": "The device authorization user code."
},
"required": false,
"description": "The device authorization user code.",
"name": "user_code",
"in": "query"
},
{
"schema": {
"type": "string",
"enum": [
"1"
],
"description": "Force a redirect to the web UI, for clients that do not send Sec-Fetch-
headers."
},
"required": false,
"description": "Force a redirect to the web UI, for clients that do not send Sec-Fetch-* headers.",
"name": "ui",
"in": "query"
}
],
"responses": {
"200": {
"description": "Device authorization payload from Better Auth"
},
"302": {
"description": "Redirects the browser to the web app device screen"
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/mcp/register": {
"post": {
"operationId": "registerMcpOAuthClient",
"tags": [
"MCP"
],
"summary": "Register MCP OAuth client",
"description": "Dynamically register a public OAuth client for the MCP endpoint. Public clients hold no secret, so authorization is protected by PKCE and an explicit consent step.",
"security": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"redirect_uris": {
"type": "array",
"items": {
"type": "string",
"maxLength": 2048
},
"minItems": 1,
"maxItems": 10
},
"client_name": {
"type": "string",
"maxLength": 100
},
"token_endpoint_auth_method": {
"type": "string",
"enum": [
"none"
]
},
"grant_types": {
"type": "array",
"items": {
"type": "string",
"enum": [
"authorization_code",
"refresh_token"
]
},
"maxItems": 2
},
"response_types": {
"type": "array",
"items": {
"type": "string",
"enum": [
"code"
]
},
"minItems": 1,
"maxItems": 1
}
},
"required": [
"redirect_uris"
]
}
}
}
},
"responses": {
"200": {
"description": "Registered OAuth client",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/McpClientRegistration"
}
}
}
},
"400": {
"description": "Invalid client metadata",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthError"
}
}
}
}
}
}
},
"/mcp/authorize": {
"get": {
"operationId": "authorizeMcpOAuthClient",
"tags": [
"MCP"
],
"summary": "Start MCP authorization",
"description": "Begin an MCP OAuth authorization. Redirects the browser to the Kaneo consent page, which then approves or denies the request.",
"security": [],
"parameters": [
{
"schema": {
"type": "string",
"enum": [
"code"
]
},
"required": true,
"name": "response_type",
"in": "query"
},
{
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"required": true,
"name": "client_id",
"in": "query"
},
{
"schema": {
"type": "string",
"maxLength": 2048
},
"required": true,
"name": "redirect_uri",
"in": "query"
},
{
"schema": {
"type": "string",
"pattern": "^[A-Za-z0-9_-]{43}$",
"description": "PKCE challenge; only S256 is accepted."
},
"required": true,
"description": "PKCE challenge; only S256 is accepted.",
"name": "code_challenge",
"in": "query"
},
{
"schema": {
"type": "string",
"enum": [
"S256"
]
},
"required": true,
"name": "code_challenge_method",
"in": "query"
},
{
"schema": {
"type": "string",
"description": "Opaque client state, returned unchanged. The complete authorization URL is limited to 8 KiB."
},
"required": false,
"description": "Opaque client state, returned unchanged. The complete authorization URL is limited to 8 KiB.",
"name": "state",
"in": "query"
}
],
"responses": {
"302": {
"description": "Redirect to the Kaneo consent page"
},
"400": {
"description": "Invalid authorization request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthError"
}
}
}
}
}
}
},
"/mcp/authorize/request/{requestId}": {
"get": {
"operationId": "getMcpAuthorizationRequest",
"tags": [
"MCP"
],
"summary": "Get consent request",
"description": "Get the client name and redirect URI for a pending consent request, so the consent page can show who is asking.",
"security": [],
"parameters": [
{
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"required": true,
"name": "requestId",
"in": "path"
}
],
"responses": {
"200": {
"description": "Authorization request details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/McpAuthorizationRequest"
}
}
}
},
"400": {
"description": "Invalid OAuth client",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthError"
}
}
}
},
"404": {
"description": "Unknown or expired authorization request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthError"
}
}
}
}
}
},
"post": {
"operationId": "decideMcpAuthorizationRequest",
"tags": [
"MCP"
],
"summary": "Decide consent request",
"description": "Approve or deny a pending consent request and get the URL to send the browser back to.",
"parameters": [
{
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"required": true,
"name": "requestId",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"approved": {
"type": "boolean"
}
},
"required": [
"approved"
]
}
}
}
},
"responses": {
"200": {
"description": "OAuth client redirect",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/McpAuthorizationDecision"
}
}
}
},
"400": {
"description": "Invalid request or OAuth client",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthError"
}
}
}
},
"401": {
"description": "Authentication required",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthError"
}
}
}
},
"403": {
"description": "Untrusted request origin",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthError"
}
}
}
},
"404": {
"description": "Unknown or expired authorization request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/oauth/id-token": {
"get": {
"operationId": "getOAuthIdToken",
"tags": [
"Authentication"
],
"summary": "Get OAuth id token",
"description": "Get the id_token for the current user's custom OAuth account. Returns null when the user signed in another way.",
"responses": {
"200": {
"description": "The id_token if available",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthIdToken"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/project/{id}/move": {
"put": {
"operationId": "moveProject",
"tags": [
"Projects"
],
"summary": "Move a project to another workspace",
"description": "Move a project and its tasks. Requires update and delete permission in the source, plus project creation and workspace settings management permission in the destination. Remove cross-project task relationships before moving.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"minLength": 1
}
},
"required": [
"workspaceId"
]
}
}
}
},
"responses": {
"200": {
"description": "Project moved",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MovedProject"
}
}
}
},
"400": {
"description": "Invalid destination or same workspace",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"402": {
"description": "Destination workspace plan has expired",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing permission in the source or destination workspace",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project or destination workspace not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Project key conflict or cross-project task relationships",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/project": {
"get": {
"operationId": "listProjects",
"tags": [
"Projects"
],
"summary": "List projects",
"description": "List a workspace's projects in sidebar order, each with rollup task statistics. Archived projects are excluded unless includeArchived is set.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "workspaceId",
"in": "query"
},
{
"schema": {
"type": "string",
"description": "Pass "true" to include archived projects in the list."
},
"required": false,
"description": "Pass "true" to include archived projects in the list.",
"name": "includeArchived",
"in": "query"
}
],
"responses": {
"200": {
"description": "List of projects",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ProjectListItem"
}
}
}
}
},
"400": {
"description": "Workspace ID could not be determined",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Workspace not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
},
"post": {
"operationId": "createProject",
"tags": [
"Projects"
],
"summary": "Create project",
"description": "Create a project in a workspace. The slug becomes the prefix of its task identifiers.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"workspaceId": {
"type": "string"
},
"icon": {
"type": "string"
},
"slug": {
"type": "string"
}
},
"required": [
"name",
"workspaceId",
"icon",
"slug"
]
}
}
}
},
"responses": {
"200": {
"description": "The created project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"400": {
"description": "Invalid body, or workspace ID could not be determined",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "Missing project:create permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Workspace not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Another project in the workspace uses this key",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/project/{id}": {
"get": {
"operationId": "getProject",
"tags": [
"Projects"
],
"summary": "Get project",
"description": "Get a single project by ID.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"200": {
"description": "Project details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
},
"put": {
"operationId": "updateProject",
"tags": [
"Projects"
],
"summary": "Update project",
"description": "Replace a project's name, icon, slug, description, and visibility.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"icon": {
"type": "string"
},
"slug": {
"type": "string"
},
"description": {
"type": "string"
},
"isPublic": {
"type": "boolean"
}
},
"required": [
"name",
"icon",
"slug",
"description",
"isPublic"
]
}
}
}
},
"responses": {
"200": {
"description": "The updated project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"400": {
"description": "Invalid body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, missing project:update, or missing project:share when visibility changes",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Another project in the workspace uses the new key",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
},
"delete": {
"operationId": "deleteProject",
"tags": [
"Projects"
],
"summary": "Delete project",
"description": "Permanently delete a project and everything in it. Archive it instead to keep the data.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"200": {
"description": "The deleted project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project:delete permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/project/{id}/background": {
"get": {
"operationId": "getProjectBackground",
"tags": [
"Projects"
],
"summary": "Download project background",
"description": "Download the current project board background image.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"200": {
"description": "The project background image",
"content": {
"image/*": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"304": {
"description": "Not modified"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project or project background not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
},
"delete": {
"operationId": "deleteProjectBackground",
"tags": [
"Projects"
],
"summary": "Delete project background",
"description": "Remove the current project board background image.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"204": {
"description": "Project background removed"
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/project/reorder": {
"put": {
"operationId": "reorderProjects",
"tags": [
"Projects"
],
"summary": "Reorder projects",
"description": "Set the sidebar order of a workspace's projects. The given positions express relative order only -- the workspace is renumbered to 0..n-1.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "workspaceId",
"in": "query"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"projects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"position": {
"type": "integer",
"minimum": 0
}
},
"required": [
"id",
"position"
]
},
"minItems": 1
}
},
"required": [
"projects"
]
}
}
}
},
"responses": {
"200": {
"description": "The reordered projects",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Project"
}
}
}
}
},
"400": {
"description": "Invalid body, or workspace ID could not be determined",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "Missing project:update permission, or no access to a listed project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Workspace not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/project/{id}/archive": {
"put": {
"operationId": "archiveProject",
"tags": [
"Projects"
],
"summary": "Archive project",
"description": "Hide a project from the default list without deleting it. Reversible with unarchive.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"200": {
"description": "The archived project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/project/{id}/unarchive": {
"put": {
"operationId": "unarchiveProject",
"tags": [
"Projects"
],
"summary": "Unarchive project",
"description": "Return an archived project to the default list.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"200": {
"description": "The restored project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Another project in the workspace uses this key",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/project/{id}/background-upload": {
"put": {
"operationId": "uploadProjectBackground",
"tags": [
"Projects"
],
"summary": "Prepare project background upload",
"description": "Create a presigned background image upload URL for a project.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"contentType": {
"type": "string"
},
"size": {
"type": "number"
}
},
"required": [
"contentType",
"size"
]
}
}
}
},
"responses": {
"200": {
"description": "Background image upload URL",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProjectBackgroundUpload"
}
}
}
},
"400": {
"description": "Invalid image upload request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
},
"503": {
"description": "Image uploads are not configured",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/project/{id}/background-upload/finalize": {
"post": {
"operationId": "finalizeProjectBackgroundUpload",
"tags": [
"Projects"
],
"summary": "Finalize project background upload",
"description": "Save an uploaded image as the project's board background.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"contentType": {
"type": "string"
},
"version": {
"type": "string"
},
"size": {
"type": "number"
}
},
"required": [
"key",
"contentType",
"version",
"size"
]
}
}
}
},
"responses": {
"200": {
"description": "Finalized project background",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProjectBackgroundFinalize"
}
}
}
},
"400": {
"description": "Invalid image upload request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
},
"500": {
"description": "Failed to save the project background",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/calendar-feed/project/{projectId}": {
"get": {
"operationId": "listCalendarFeeds",
"tags": [
"Calendar feeds"
],
"summary": "List project calendar feeds",
"description": "List secret calendar subscription links. Requires project sharing permission.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
}
],
"responses": {
"200": {
"description": "Calendar feeds",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CalendarFeed"
}
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Authentication required",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project sharing permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
},
"post": {
"operationId": "createCalendarFeed",
"tags": [
"Calendar feeds"
],
"summary": "Create a calendar feed",
"description": "Create a calendar subscription matching any selected label, or all project tasks when no labels are selected. Tasks need a start or due date. All-day dates use the supplied time zone. Creating a missing workspace label definition also requires label:create permission.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"labelIds": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"maxItems": 100,
"default": [],
"description": "Match any selected label. Omit or leave empty to include all scheduled project tasks."
},
"timeZone": {
"type": "string",
"default": "UTC"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Calendar feed created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CalendarFeed"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Authentication required",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, missing project:share permission, or missing label:create permission for a new workspace label definition",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/calendar-feed/project/{projectId}/{id}": {
"delete": {
"operationId": "revokeCalendarFeed",
"tags": [
"Calendar feeds"
],
"summary": "Revoke a calendar feed",
"description": "Revoke a calendar subscription link, preventing further access through it.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
},
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"200": {
"description": "Calendar feed revoked",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
}
},
"required": [
"success"
]
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Authentication required",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project sharing permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project or calendar feed not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/{id}/description": {
"get": {
"operationId": "getTaskDescriptionPage",
"tags": [
"Tasks"
],
"summary": "Read a description page",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
},
{
"schema": {
"type": "integer",
"minimum": 0,
"maximum": 2000000000,
"default": 0
},
"required": false,
"name": "offset",
"in": "query"
},
{
"schema": {
"type": "string",
"pattern": "^[0-9]{1,10}$"
},
"required": false,
"name": "version",
"in": "query"
}
],
"responses": {
"200": {
"description": "Description page",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskDescriptionPage"
}
}
}
},
"400": {
"description": "Invalid offset or version",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Description changed; reload from offset zero",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
},
"503": {
"description": "Description request timed out",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/task/description-matches/{projectId}": {
"get": {
"operationId": "findDeferredTaskDescriptions",
"tags": [
"Tasks"
],
"summary": "Search descriptions omitted from task lists",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
},
{
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 256
},
"required": true,
"name": "query",
"in": "query"
},
{
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"required": false,
"name": "after",
"in": "query"
}
],
"responses": {
"200": {
"description": "Matching task IDs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskDescriptionMatches"
}
}
}
},
"400": {
"description": "Invalid search query or cursor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
},
"503": {
"description": "Description search timed out",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/task/assigned": {
"get": {
"operationId": "listAssignedTasks",
"tags": [
"Tasks"
],
"summary": "List my assigned tasks",
"description": "Get the open tasks assigned to the caller across a workspace's active projects. Completed and archived tasks are excluded. Ordered by due date (undated last), then priority. At most 100 tasks are returned; total counts all of them. Set countOnly=true to return just that total without loading task rows or labels.",
"parameters": [
{
"schema": {
"type": "string",
"minLength": 1
},
"required": true,
"name": "workspaceId",
"in": "query"
},
{
"schema": {
"type": "string",
"enum": [
"true",
"false"
],
"description": "Return only the total, with an empty tasks array, without loading task rows or labels."
},
"required": false,
"description": "Return only the total, with an empty tasks array, without loading task rows or labels.",
"name": "countOnly",
"in": "query"
}
],
"responses": {
"200": {
"description": "Open tasks assigned to the caller",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssignedTasks"
}
}
}
},
"400": {
"description": "Invalid workspace ID or query parameters",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "Missing project:read or task:read permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Workspace not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/tasks/{projectId}": {
"get": {
"operationId": "listTasks",
"tags": [
"Tasks"
],
"summary": "List tasks",
"description": "Get a project's board: its columns, each with the tasks in it, plus the archived and planned buckets. Responses always contain at most 100 tasks (50 by default). Continue through pagination.totalPages for the whole board, and for each task page follow relatedPage through pagination.relatedTotalPages for all labels, links and column metadata. Filters and sorting apply before pagination. Descriptions larger than 64 KiB are omitted with descriptionDeferred=true; read the task detail or description pages for full text.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
},
{
"schema": {
"type": "string"
},
"required": false,
"name": "status",
"in": "query"
},
{
"schema": {
"type": "string"
},
"required": false,
"name": "priority",
"in": "query"
},
{
"schema": {
"type": "string"
},
"required": false,
"name": "assigneeId",
"in": "query"
},
{
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 1000000
},
"required": false,
"name": "page",
"in": "query"
},
{
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 1000000
},
"required": false,
"name": "relatedPage",
"in": "query"
},
{
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 100
},
"required": false,
"name": "limit",
"in": "query"
},
{
"schema": {
"type": "string",
"enum": [
"createdAt",
"priority",
"dueDate",
"position",
"title",
"number"
]
},
"required": false,
"name": "sortBy",
"in": "query"
},
{
"schema": {
"type": "string",
"enum": [
"asc",
"desc"
]
},
"required": false,
"name": "sortOrder",
"in": "query"
},
{
"schema": {
"type": "string"
},
"required": false,
"name": "dueBefore",
"in": "query"
},
{
"schema": {
"type": "string"
},
"required": false,
"name": "dueAfter",
"in": "query"
}
],
"responses": {
"200": {
"description": "The project board",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BoardResponse"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
},
"503": {
"description": "Task list request timed out",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/task/bulk": {
"patch": {
"operationId": "bulkUpdateTasks",
"tags": [
"Tasks"
],
"summary": "Bulk update tasks",
"description": "Apply one operation to many tasks at once. Every task must be in the same workspace.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"taskIds": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 1
},
"operation": {
"type": "string",
"enum": [
"updateStatus",
"updatePriority",
"updateAssignee",
"delete",
"addLabel",
"removeLabel",
"updateDueDate"
]
},
"value": {
"type": [
"string",
"null"
],
"description": "The new value for the chosen operation. Unused by delete; null clears an assignee or due date."
}
},
"required": [
"taskIds",
"operation"
]
}
}
}
},
"responses": {
"200": {
"description": "Bulk operation result",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BulkTaskResult"
}
}
}
},
"400": {
"description": "Invalid body, or the tasks span more than one workspace",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing the permission the operation needs",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "No tasks found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Tasks changed projects; retry the operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/draft-upload/{projectId}": {
"post": {
"operationId": "stageTaskAssetUpload",
"tags": [
"Tasks"
],
"summary": "Stage a task attachment",
"description": "Upload an attachment before submitting a task. Staged assets are private to the uploader and expire after 24 hours.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"filename": {
"type": "string"
},
"contentType": {
"type": "string"
},
"size": {
"type": "number"
},
"surface": {
"type": "string",
"enum": [
"description"
]
}
},
"required": [
"filename",
"contentType",
"size",
"surface"
]
}
}
}
},
"responses": {
"200": {
"description": "Presigned upload",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskImageUpload"
}
}
}
},
"400": {
"description": "Invalid upload",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "Missing task:create permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
},
"503": {
"description": "Storage unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/task/draft-upload/{projectId}/finalize": {
"post": {
"operationId": "finalizeStagedTaskAsset",
"tags": [
"Tasks"
],
"summary": "Finalize a staged task attachment",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key returned when the URL was issued."
},
"filename": {
"type": "string"
},
"contentType": {
"type": "string"
},
"size": {
"type": "number"
},
"surface": {
"type": "string",
"enum": [
"description"
]
}
},
"required": [
"key",
"filename",
"contentType",
"size",
"surface"
]
}
}
}
},
"responses": {
"200": {
"description": "Staged attachment",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskImageAsset"
}
}
}
},
"400": {
"description": "Invalid upload",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "Missing task:create permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Upload already attached",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
},
"503": {
"description": "Storage unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/task/reorder": {
"post": {
"operationId": "reorderTasks",
"tags": [
"Tasks"
],
"summary": "Reorder or move cards",
"description": "Atomically update positions and optional column status for cards in one project. Other task fields are preserved.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"projectId": {
"type": "string"
},
"expectedTasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"position": {
"type": [
"integer",
"null"
],
"minimum": 0,
"maximum": 2147483646
},
"status": {
"type": "string"
}
},
"required": [
"id",
"position",
"status"
]
},
"description": "Previous complete contents of the affected columns; stale snapshots return 409"
},
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"position": {
"type": "integer",
"minimum": 0,
"maximum": 2147483646
},
"status": {
"type": "string"
}
},
"required": [
"id",
"position"
]
},
"minItems": 1
}
},
"required": [
"projectId",
"tasks"
]
}
}
}
},
"responses": {
"200": {
"description": "Updated card positions and statuses",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"position": {
"type": "number"
},
"status": {
"type": "string"
}
},
"required": [
"id",
"position",
"status"
]
}
}
}
}
},
"400": {
"description": "Invalid positions or column",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "Missing task:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found, or tasks do not belong to the project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Board changed or task moved; refresh before reordering",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/{projectId}": {
"post": {
"operationId": "createTask",
"tags": [
"Tasks"
],
"summary": "Create task",
"description": "Add a task to a project. It is placed in the column named by status.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"startDate": {
"type": "string"
},
"dueDate": {
"type": "string"
},
"priority": {
"type": "string",
"enum": [
"no-priority",
"low",
"medium",
"high",
"urgent"
]
},
"status": {
"type": "string",
"description": "The target column's slug."
},
"userId": {
"type": "string",
"description": "Assignee, if any."
},
"draftAssetIds": {
"type": "array",
"items": {
"type": "string"
},
"maxItems": 100
},
"customFields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"fieldId": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"fieldId",
"value"
]
}
}
},
"required": [
"title",
"description",
"priority",
"status"
]
}
}
}
},
"responses": {
"200": {
"description": "The created task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"400": {
"description": "Invalid body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:create permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/duplicate/{id}": {
"post": {
"operationId": "duplicateTask",
"tags": [
"Tasks"
],
"summary": "Duplicate a task",
"description": "Copy a task into the same project and column, including custom fields, labels, description assets and same-workspace parent links. Copying parent links also requires task:update. Comments, time entries and child tasks are not copied.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "The duplicated task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"400": {
"description": "Invalid task fields or request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, missing task:create, or missing task:update when copying parent links",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task or project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Task column has reached its capacity",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
},
"503": {
"description": "Unable to copy task attachments",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/task/by-ticket-id/{ticketId}": {
"get": {
"operationId": "getTaskByTicketId",
"tags": [
"Tasks"
],
"summary": "Get task by ticket ID",
"description": "Get a single task by its project key and number, such as KAN-12. A match in an active project takes precedence, then the most recently archived project. If the ticket ID still matches multiple accessible tasks, narrow the lookup to a workspace with workspaceId or workspaceSlug, or to a single project with projectId.",
"parameters": [
{
"schema": {
"type": "string",
"maxLength": 128,
"description": "Project key and task number, e.g. KAN-12."
},
"required": true,
"description": "Project key and task number, e.g. KAN-12.",
"name": "ticketId",
"in": "path"
},
{
"schema": {
"type": "string",
"minLength": 1,
"description": "Select a workspace if the ticket ID exists in more than one."
},
"required": false,
"description": "Select a workspace if the ticket ID exists in more than one.",
"name": "workspaceId",
"in": "query"
},
{
"schema": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"description": "Select a workspace by its slug instead of its ID."
},
"required": false,
"description": "Select a workspace by its slug instead of its ID.",
"name": "workspaceSlug",
"in": "query"
},
{
"schema": {
"type": "string",
"minLength": 1,
"description": "Select a project if the ticket ID exists more than once."
},
"required": false,
"description": "Select a project if the ticket ID exists more than once.",
"name": "projectId",
"in": "query"
}
],
"responses": {
"200": {
"description": "Task details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskByTicketId"
}
}
}
},
"400": {
"description": "Invalid ticket ID",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "No accessible task has this ticket ID",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Ticket ID matches multiple accessible tasks",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/{id}": {
"get": {
"operationId": "getTask",
"tags": [
"Tasks"
],
"summary": "Get task",
"description": "Get a single task by ID, with its assignee name. The board view omits descriptions above 64 KiB and includes task and same-project parent subtask progress.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
},
{
"schema": {
"type": "string",
"enum": [
"detail",
"board"
]
},
"required": false,
"name": "view",
"in": "query"
}
],
"responses": {
"200": {
"description": "Task details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskWithAssignee"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
},
"put": {
"operationId": "updateTask",
"tags": [
"Tasks"
],
"summary": "Update task",
"description": "Replace every field of a task. Use the single-field routes for narrower edits.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string",
"description": "Omit to preserve the existing description when updating a list summary."
},
"startDate": {
"type": "string"
},
"dueDate": {
"type": "string"
},
"priority": {
"type": "string",
"enum": [
"no-priority",
"low",
"medium",
"high",
"urgent"
]
},
"status": {
"type": "string"
},
"projectId": {
"type": "string"
},
"position": {
"type": "integer",
"minimum": 0,
"maximum": 2147483646
},
"userId": {
"type": "string"
}
},
"required": [
"title",
"priority",
"status",
"projectId",
"position"
]
}
}
}
},
"responses": {
"200": {
"description": "The updated task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"400": {
"description": "Invalid body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:update or task:assign permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
},
"delete": {
"operationId": "deleteTask",
"tags": [
"Tasks"
],
"summary": "Delete task",
"description": "Permanently delete a task and its comments, labels, and time entries.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"200": {
"description": "The deleted task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:delete permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Task changed projects; retry the operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/move/{id}": {
"put": {
"operationId": "moveTask",
"tags": [
"Tasks"
],
"summary": "Move task",
"description": "Move a task to another project, optionally into a named column. Both projects must be in the same workspace.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"destinationProjectId": {
"type": "string"
},
"destinationStatus": {
"type": "string",
"description": "Defaults to the destination project's first column."
}
},
"required": [
"destinationProjectId"
]
}
}
}
},
"responses": {
"200": {
"description": "The moved task, with both project ids",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MoveTaskResult"
}
}
}
},
"400": {
"description": "Invalid body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task or destination project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "Task or project moved concurrently; retry the move",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/export/{projectId}": {
"get": {
"operationId": "exportTasks",
"tags": [
"Tasks"
],
"summary": "Export tasks",
"description": "Export a project's tasks, with their labels, as a JSON document.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
}
],
"responses": {
"200": {
"description": "The exported project and tasks",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskExport"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/import/{projectId}": {
"post": {
"operationId": "importTasks",
"tags": [
"Tasks"
],
"summary": "Import tasks",
"description": "Import tasks into a project. Each task is reported individually, so a partial import still returns 200.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"status": {
"type": "string",
"description": "Column slug, current column name, or slugified current name. Unknown values map to planned with a warning; ambiguous names fail the task import."
},
"priority": {
"type": "string"
},
"startDate": {
"type": [
"string",
"null"
]
},
"dueDate": {
"type": [
"string",
"null"
]
},
"userId": {
"type": [
"string",
"null"
],
"description": "Assignable user's ID or email address. Emails match case-insensitively. Null, empty, or omitted leaves the task unassigned."
}
},
"required": [
"title",
"status"
]
}
}
},
"required": [
"tasks"
]
}
}
}
},
"responses": {
"200": {
"description": "Per-task import outcome",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskImportResult"
}
}
}
},
"400": {
"description": "Invalid body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:create permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/status/{id}": {
"put": {
"operationId": "updateTaskStatus",
"tags": [
"Tasks"
],
"summary": "Update task status",
"description": "Move a task to another column in the same project.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string"
}
},
"required": [
"status"
]
}
}
}
},
"responses": {
"200": {
"description": "The updated task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"400": {
"description": "Invalid body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/priority/{id}": {
"put": {
"operationId": "updateTaskPriority",
"tags": [
"Tasks"
],
"summary": "Update task priority",
"description": "Set a task's priority.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"priority": {
"type": "string",
"enum": [
"no-priority",
"low",
"medium",
"high",
"urgent"
]
}
},
"required": [
"priority"
]
}
}
}
},
"responses": {
"200": {
"description": "The updated task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"400": {
"description": "Invalid priority",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/assignee/{id}": {
"put": {
"operationId": "updateTaskAssignee",
"tags": [
"Tasks"
],
"summary": "Update task assignee",
"description": "Assign a task to a workspace member, or send null to unassign it.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"userId": {
"type": [
"string",
"null"
],
"description": "Null unassigns."
}
},
"required": [
"userId"
]
}
}
}
},
"responses": {
"200": {
"description": "The updated task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"400": {
"description": "Invalid body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:assign permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found, or assignee is not a member of the workspace",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/due-date/{id}": {
"put": {
"operationId": "updateTaskDueDate",
"tags": [
"Tasks"
],
"summary": "Update task due date",
"description": "Set or clear a task's due date.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"dueDate": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "The updated task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"400": {
"description": "Invalid date",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/title/{id}": {
"put": {
"operationId": "updateTaskTitle",
"tags": [
"Tasks"
],
"summary": "Update task title",
"description": "Rename a task.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string"
}
},
"required": [
"title"
]
}
}
}
},
"responses": {
"200": {
"description": "The updated task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"400": {
"description": "Invalid body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/image-upload/{id}": {
"put": {
"operationId": "createTaskImageUpload",
"tags": [
"Tasks"
],
"summary": "Create image upload URL",
"description": "Get a presigned URL for uploading an image used in a task description or comment. PUT the bytes to it, then call the finalize route.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"filename": {
"type": "string"
},
"contentType": {
"type": "string"
},
"size": {
"type": "number"
},
"surface": {
"type": "string",
"enum": [
"description",
"comment"
],
"description": "Where the image is used, which decides how it is scoped."
}
},
"required": [
"filename",
"contentType",
"size",
"surface"
]
}
}
}
},
"responses": {
"200": {
"description": "The presigned upload",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskImageUpload"
}
}
}
},
"400": {
"description": "Unsupported content type, or the file is too large",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
},
"503": {
"description": "Image uploads are not configured on this instance",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/task/image-upload/{id}/finalize": {
"post": {
"operationId": "finalizeTaskImageUpload",
"tags": [
"Tasks"
],
"summary": "Finalize image upload",
"description": "Record an uploaded image as a private asset and return the URL to reference it by.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key returned when the URL was issued."
},
"filename": {
"type": "string"
},
"contentType": {
"type": "string"
},
"size": {
"type": "number"
},
"surface": {
"type": "string",
"enum": [
"description",
"comment"
],
"description": "Where the image is used, which decides how it is scoped."
}
},
"required": [
"key",
"filename",
"contentType",
"size",
"surface"
]
}
}
}
},
"responses": {
"200": {
"description": "The stored asset",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TaskImageAsset"
}
}
}
},
"400": {
"description": "Invalid upload, or the key does not belong to this task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/task/description/{id}": {
"put": {
"operationId": "updateTaskDescription",
"tags": [
"Tasks"
],
"summary": "Update task description",
"description": "Replace a task's description.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"description": {
"type": "string"
}
},
"required": [
"description"
]
}
}
}
},
"responses": {
"200": {
"description": "The updated task",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Task"
}
}
}
},
"400": {
"description": "Invalid body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing task:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Task not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/column/{projectId}": {
"get": {
"operationId": "getColumns",
"tags": [
"Columns"
],
"summary": "Get columns",
"description": "Get a project's board columns, ordered by position.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
}
],
"responses": {
"200": {
"description": "List of columns ordered by position",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Column"
}
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
},
"post": {
"operationId": "createColumn",
"tags": [
"Columns"
],
"summary": "Create column",
"description": "Add a column to the end of a project's board. The slug is derived from the name, with a numeric suffix such as -2 when another column in the project already uses it.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"icon": {
"type": "string"
},
"color": {
"type": "string"
},
"isFinal": {
"type": "boolean"
}
},
"required": [
"name"
]
}
}
}
},
"responses": {
"200": {
"description": "The created column",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Column"
}
}
}
},
"400": {
"description": "Invalid body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "The slug is reserved for a virtual task status",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/column/reorder/{projectId}": {
"put": {
"operationId": "reorderColumns",
"tags": [
"Columns"
],
"summary": "Reorder columns",
"description": "Set new positions for a project's columns and return the whole board in its new order.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "projectId",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"columns": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"position": {
"type": "number"
}
},
"required": [
"id",
"position"
]
},
"description": "Every column keeps its new position. Columns from another project are rejected."
}
},
"required": [
"columns"
]
}
}
}
},
"responses": {
"200": {
"description": "The reordered columns",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Column"
}
}
}
}
},
"400": {
"description": "A column does not belong to this project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Project not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/column/{id}": {
"put": {
"operationId": "updateColumn",
"tags": [
"Columns"
],
"summary": "Update column",
"description": "Update a column. Omitted fields are left unchanged; icon and color accept null to clear them.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"icon": {
"type": [
"string",
"null"
]
},
"color": {
"type": [
"string",
"null"
]
},
"isFinal": {
"type": "boolean"
}
}
}
}
}
},
"responses": {
"200": {
"description": "The updated column",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Column"
}
}
}
},
"400": {
"description": "Invalid body",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Column not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
},
"delete": {
"operationId": "deleteColumn",
"tags": [
"Columns"
],
"summary": "Delete column",
"description": "Delete an empty column. A column holding tasks is refused until they are moved or deleted.",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "id",
"in": "path"
}
],
"responses": {
"200": {
"description": "The deleted column",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Column"
}
}
}
},
"401": {
"description": "Missing or invalid credentials",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"403": {
"description": "No access to the project, or missing project:update permission",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "Column not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "The column still contains tasks",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"429": {
"$ref": "#/components/responses/ApiKeyRateLimited"
}
}
}
},
"/activity/workspace/{workspaceId}": {
"get": {
"operationId": "getWorkspaceActivities",
"tags": [
"Activity"
],
"summary": "Get recent workspace activity",
"description": "Get the 20 most recent task events across a workspace's active projects from the last 30 days, newest first. Each event carries a short plain-text excerpt instead of its full content.",
"parameters": [
{
"schema": {
"type": "string",
"minLength": 1
},
"required": true,
"name": "workspaceId",
"in": "path"
}
],
"responses": {
"200": {
"description": "Recent activity in the workspace",
"content": {
"application/json": {
"