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
- Cloud: Get started with Kaneo Cloud.
- Self-hosted: Follow the installation guide.
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. Bothkaneo-apiandkaneo-webare 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.tscallSentry.captureCheckInwith 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 ofprovision-sentry-alerts.shwill resolve the slugs and create the cron-missed workflow.
Applying the alerts
Option A: provisioning script (recommended)
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.
# 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-runThe 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:
- Settings > Alerts > [rule name] > Actions
- Pick a notification target. Recommended:
- Slack
#engineeringfor spike/latency/cron-missed - Email to on-call for new issues and MCP errors
- Slack
- 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 viadetectorIds.
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.
SENTRY_API_TOKEN=sntrys_... ./scripts/provision-sentry-dashboards.sh
SENTRY_API_TOKEN=sntrys_... ./scripts/provision-sentry-dashboards.sh --dry-runThe 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 viadetectorIds.
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 namewidgetsโ array of{ title, displayType, interval, queries }objectsqueriesโ array of{ fields, aggregates, columns, query, orderby, limit }fieldsโ columns to displayaggregatesโ aggregate functions to compute (e.g.,count(),p95(transaction.duration))columnsโ group-by fieldsqueryโ 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:
- https://docs.sentry.io/api/monitors/create-an-alert-for-an-organization/
- https://docs.sentry.io/api/monitors/create-a-monitor-for-a-project/
- https://docs.sentry.io/api/dashboards/create-a-new-dashboard-for-an-organization/
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:
pnpm dlx mint dev
pnpm dlx mint validate
pnpm dlx mint broken-linksThe 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.comorhttp://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:
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 buildThe 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:
pnpm --filter @kaneo/mobile lint
pnpm --filter @kaneo/mobile typecheck
pnpm --filter @kaneo/mobile testRelease 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:
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 secretCI 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
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 pluginsOverview (`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:
helm install kaneo oci://ghcr.io/usekaneo/charts/kaneo \
--namespace kaneo \
--create-namespace
# Access locally
kubectl port-forward svc/kaneo-kaneo 5173:5173 -n kaneoOpen http://localhost:5173 and you're ready to go.
Production Setup with Ingress
For real deployments, you'll want proper ingress:
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:
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:
helm install my-kaneo oci://ghcr.io/usekaneo/charts/kaneoTo install from a local checkout instead:
helm install my-kaneo ./charts/kaneoThe 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:
helm uninstall my-kaneoParameters
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
# 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: 5173Production Configuration with TLS
# 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.comProduction 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:
# values.yaml
kaneo:
env:
authSecret: "your-secure-auth-secret-at-least-32-characters"
clientUrl: "https://kaneo.your-domain.com"# 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-tlsUsing External PostgreSQL Database
If you prefer to use an external PostgreSQL database instead of the bundled one:
# 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.
# 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:
# 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.
# 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"# 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_uriDatabase 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 settingpostgresql.enabled=falseand configuringkaneo.env.database.external.
Bundled PostgreSQL credentials are only applied when PostgreSQL initializes an empty data directory. If a PVC already exists, changingpostgresql.auth.passwordorpostgresql.auth.existingSecretupdates 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:
helm uninstall kaneo -n kaneo-test
kubectl delete pvc kaneo-postgresql-data -n kaneo-test --ignore-not-foundTo 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:
# Example backup command
kubectl exec -it deployment/my-kaneo-postgresql -- pg_dump -U kaneo_user kaneo > kaneo-backup.sqlMigration from SQLite
If you're migrating from a previous SQLite-based installation, you'll need to:
- Export your data from SQLite
- Deploy the new PostgreSQL-based chart
- 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:
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 requiredclientUrl.
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:
kubectl get secret kaneo-secrets -n kaneo \
-o jsonpath='{.data.postgres_uri}' | base64 -dCommon 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:
- Kaneo application: Serves the web UI and API from the combined Kaneo image
- 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:
- Set secure values for
AUTH_SECRETand PostgreSQL passwords - Use an Ingress controller to expose the application
- Configure TLS for secure access
- Set appropriate resource limits and requests
- Enable persistent storage with appropriate storage classes
- Consider using external PostgreSQL for better scalability
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.comUsing Gateway API
As an alternative to Ingress, the chart can create an HTTPRoute for the Kubernetes Gateway API:
# values.yaml
gateway:
enabled: true
parentRefs:
- name: main-gateway
namespace: gateway-system
sectionName: https
hostnames:
- kaneo.example.comBy default the chart creates one Gateway API rule:
/goes to the Kaneo service
If you need custom matching or multiple backend references, overridegateway.rulesdirectly. EachbackendRefsentry follows the same pattern as ingress and uses the chart-specificservicefield (kaneo), which is expanded to the release-specific Service name.
Security
For production deployments, consider the following security recommendations:
- Use secure
AUTH_SECRETand PostgreSQL passwords, preferably stored in Kubernetes Secrets - Enable TLS for ingress or Gateway API
- Enable and set resource limits to prevent resource exhaustion
- Use a dedicated storage class for the PostgreSQL database
- Consider using a network policy to restrict traffic between components
- Regularly update PostgreSQL and application images
Pod Security Context
On clusters with Pod Security Admission enforcement, set the following to satisfy the restricted policy:
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:
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:
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:latestFor 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:
kaneo:
env:
disableRegistration: trueThis 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.
curl -fsSL https://kaneo.app/cli/install.sh | sh
kaneo login
kaneo task listIt 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:
curl -fsSL https://kaneo.app/cli/install.sh | shWindows (PowerShell):
irm https://kaneo.app/cli/install.ps1 | iexWith Homebrew, or with npm and Node.js 20 or newer:
brew install usekaneo/tap/kaneo
npm install @kaneo/cliThe 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 |
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$env:KANEO_VERSION = "0.1.0"; irm https://kaneo.app/cli/install.ps1 | iexTo 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.
kaneo login # Kaneo Cloud
kaneo login --api-url https://kaneo.example.com
kaneo whoami
kaneo logoutFor CI and scripts, use an API key instead (create one in Settings, then Account, then API Keys):
export KANEO_API_KEY=...
kaneo task list -w <workspace-id> -p KANCredentials 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:
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.
npx skills add usekaneo/kaneo --skill kaneo-cliConfiguration
| 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:
{ "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
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 typecheckThe 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:
DEVICE_AUTH_CLIENT_IDS=kaneo-cli,kaneo-mcp,your-client-idEnvironment
| 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:
npx @kaneo/mcpnpm 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:
npx @kaneo/mcp serveIf you prefer a global install:
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):
kaneo-mcp install --target cursor-user -yPoint at a self-hosted API when generating the config:
kaneo-mcp install --target cursor-user -y --api-url https://kaneo.example.comSee all options:
kaneo-mcp install --helpIf 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:
node dist/index.jsThe published package includes dist/. prepublishOnly runs the build before publish.
Develop from source
From the repo root:
pnpm install
pnpm --filter @kaneo/mcp run build
pnpm --filter @kaneo/mcp run start
pnpm --filter @kaneo/mcp run testOr run it from the package directory:
pnpm -C packages/mcp run buildThe 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:
- Requests a device code from
POST /api/auth/device/code - Prints the verification URL and user code to
stderr - Tries to open the browser
- Polls
POST /api/auth/device/tokenuntil approved - Stores the access token at
~/.config/kaneo-mcp/credentials.jsonwith mode0600
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 nodelete_time_entry tool.
Call list_project_custom_fields for the field IDs and optionsset_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
npx @kaneo/planka-import --planka-url https://planka.example.com --dry-runA 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:
npx @kaneo/planka-import \
--planka-url https://planka.example.com \
--kaneo-url https://cloud.kaneo.app \
--workspace ws_123You'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 openready-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:
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.shInstall Chromium once usingscripts/ui-review-bot/node_modules/.bin/playwright install chromium.
Stop only this stack and remove its disposable data afterwards:
docker compose --env-file /dev/null -p kaneo-ci -f scripts/ci/compose.yml --profile realtime --profile upgrade down -vThe 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:
pnpm install --offline --frozen-lockfile --ignore-scripts --package-import-method=cloneThis 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 inpolicy.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:
cd scripts/slop-labeler
npm ci --ignore-scripts
npm testThe classifier comes from kqr's article.vendor/ preserves the upstream source at9d3abed60e628951c7b0d2cd1e1c69eddb23eb97, 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/kaneowith 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_IDand secretsAPP_PRIVATE_KEY(the complete PEM file) andOPENROUTER_API_KEY. - Put this directory and both
ui-review.ymlandui-review-run.ymlworkflows on the default branch. - Deploy
worker/to Cloudflare (see below). Enable the App webhook at its/webhookURL, 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.
npm --prefix scripts/ui-review-bot ci --ignore-scripts
npm --prefix scripts/ui-review-bot run setup
npm --prefix scripts/ui-review-bot start -- 1719A 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.
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.jsonUse --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.
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.jsoncStore 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:
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 --budgetReports 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:integrationfrom the repo root (requires Postgres andDATABASE_URL). - CI:
.github/workflows/ci.ymlstarts 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.
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 --volumesThe 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 iskaneo-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:
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_FILEBrowserStack 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
- Add repository Actions secrets
BROWSERSTACK_USERNAMEandBROWSERSTACK_ACCESS_KEY. - Merge this integration into
main. - Every push to
mainnow runs BrowserStack automatically. To rerun it,
open Actions โ BrowserStack โ Run workflow, selectingmain.
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:
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 :9000Once the service is ready, run:
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-testThe 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:
node scripts/ui-review-bot/docs-screenshots.mjsThe 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.
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?
- Board and List: create, find, and update tasks.
- Members and roles: invite people and manage access.
- Backups and recovery: protect a self-hosted installation.
- Troubleshooting: work through common setup problems.
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
curl -fsSL https://kaneo.app/cli/install.sh | sh
kaneo login
kaneoOn 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
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
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
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.
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
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| StoragePostgreSQL 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
- Put the instance behind HTTPS.
- Create the initial administrator account before opening registration to others.
- Configure email and access settings.
- Add Silo file storage if your team needs attachments.
- 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
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.
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.
Notification preferences control the events and workspace rules for all four channels.
Automation and AI assistants
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.
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": {
"