KairoProject Public API
Connect your tools to KairoProject via the public REST API. Authentication, endpoints, webhooks, and best practices for integrators.
The KairoProject public API lets your external tools and systems interact directly with your project management data: portfolios, projects, tasks, and resources. This guide covers authentication, available endpoints, outbound webhooks, and integration best practices.
API version 1
The public API is currently at version v1. The endpoints described in this guide are stable and production-ready. New features are added without breaking changes; any incompatible change will introduce a /v2/ prefix with at least 3 months' notice.
Conceptual model
The API strictly follows KairoProject's data hierarchy:
Organisation
├── API Clients ← your integrations (credentials)
├── Members ← org users
├── Webhooks ← event delivery endpoints
└── Portfolio
├── Resource ← shared across all portfolio projects
├── Team ← group of resources, shared
└── Project
├── Root Task ← structural task (cannot be deleted)
├── Task ... ← your operational tasks
└── Finish Task ← structural task (cannot be deleted)
Key points:
- All data is strictly isolated per organisation — a token can never access another organisation's data.
- Resources and teams live at portfolio level, shared across all projects.
- Each project has two structural tasks (
root,finish) that cannot be deleted via the API. - The CCPM recompute (critical chain, buffer) is triggered via
POST /projects/{projectId}/recompute?portfolioId=...and returns a synchronous response.
Base URL
https://kairoproject.app/api/public/v1
All dates are ISO 8601 UTC (2026-05-17T10:00:00.000Z).
Authentication
The API uses the OAuth 2.0 client_credentials flow. Each organization can create API clients from the admin console.
Create an API client
An owner or admin creates an API client from the console. On creation, a clientId and a clientSecret are generated — the secret is only shown once.
Each API client holds:
- a name displayed in the console;
- scopes defining the granted permissions;
- a status (
activeorrevoked); - a token lifetime (
tokenTTLSeconds, default 3,600 seconds).
Obtain an access token
POST /api/public/oauth/token
Request body:
{
"client_id": "org_acme-api-1234",
"client_secret": "kairo_sk_live_...",
"scope": "projects.read tasks.write"
}
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "projects.read tasks.write"
}
Use the token in all subsequent requests:
Authorization: Bearer <access_token>
Token lifetime
Tokens expire after 3,600 seconds by default. Manage expiry client-side by comparing issued_at + expires_in with the current time. There is no need to regenerate a token before every request.
Available scopes
| Scope | Access granted |
|---|---|
portfolios.read | Read portfolios |
portfolios.write | Create and update portfolios |
projects.read | Read projects |
projects.write | Create, update, and delete projects |
tasks.read | Read tasks |
tasks.write | Create, update, and delete tasks |
resources.read | Read resources and teams |
resources.write | Create, update, and delete resources and teams |
planning.trigger | Trigger planning recompute |
organization.read | Read organisation plan/seat information |
members.manage | Read, invite, and update members |
webhooks.manage | Manage outbound webhooks |
Rotation and revocation
- Rotation: create a new secret from the console. The new secret becomes active immediately and the previous secret is disabled during rotation.
- Revocation: revoke the client from the organisation console (API section). New token requests are rejected immediately, and existing tokens may also be invalidated as soon as revocation occurs.
Rate limiting
Each authenticated request is counted in a sliding window per API client. Limits vary based on the most restrictive scope in the request.
| Scope | Requests / minute |
|---|---|
tasks.read | 120 |
tasks.write | 60 |
projects.read | 60 |
projects.write | 30 |
planning.trigger | 20 |
webhooks.manage | 30 |
| Others | 60 |
Every successful response includes the following headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1747476120
When the limit is exceeded, the API returns 429 with a uniform JSON body in the form { "error": "..." }. Depending on the endpoint, retry timing information may also be exposed in headers.
Available endpoints
All endpoints are prefixed with /api/public/v1 and require a valid Bearer token. portfolioId is always required as a query param for portfolio sub-resources.
Legend: ✅ Idempotent (repeating the request produces the same result) — ❌ Creates a duplicate
Portfolios
| Method | Route | Required scope | Idempotent |
|---|---|---|---|
GET | /portfolios | portfolios.read | ✅ |
POST | /portfolios | portfolios.write | ❌ |
GET | /portfolios/:id | portfolios.read | ✅ |
PATCH | /portfolios/:id | portfolios.write | ✅ |
Patchable fields: name, description, aiAssistEnabled.
Projects
| Method | Route | Required scope | Idempotent |
|---|---|---|---|
GET | /projects?portfolioId= | projects.read | ✅ |
POST | /projects | projects.write | ❌ |
GET | /projects/:id?portfolioId= | projects.read | ✅ |
PATCH | /projects/:id?portfolioId= | projects.write | ✅ |
DELETE | /projects/:id?portfolioId= | projects.write | ✅ |
POST | /projects/:id/recompute?portfolioId= | planning.trigger | ✅ |
Irreversible deletion
Deleting a project removes the project and all its tasks. This operation cannot be undone. The progress and bufferConsumption fields are computed — they are read-only.
Tasks
| Method | Route | Required scope | Idempotent |
|---|---|---|---|
GET | /projects/:id/tasks?portfolioId= | tasks.read | ✅ |
POST | /projects/:id/tasks | tasks.write | ❌ |
GET | /projects/:id/tasks/:taskId?portfolioId= | tasks.read | ✅ |
PATCH | /projects/:id/tasks/:taskId?portfolioId= | tasks.write | ✅ |
DELETE | /projects/:id/tasks/:taskId?portfolioId= | tasks.write | ✅ |
Updating ettcHours (Estimated Time to Complete — remaining hours) is the progress-reporting mechanism. The root and finish tasks cannot be deleted (returns 422).
Resources and teams
| Method | Route | Required scope | Idempotent |
|---|---|---|---|
GET / POST / PATCH / DELETE | /projects/:id/resources?portfolioId= | resources.read / resources.write | ✅ / ❌ |
GET / POST / PATCH / DELETE | /projects/:id/teams?portfolioId= | resources.read / resources.write | ✅ / ❌ |
Resources and teams are stored at portfolio level. The projectId in the URL is used solely to trigger a planning recompute for the affected project after a mutation.
Organization and members
| Method | Route | Required scope | Idempotent |
|---|---|---|---|
GET | /organization | organization.read | ✅ |
GET | /members | members.manage | ✅ |
POST | /members | members.manage | ❌ |
GET | /members/:id | members.manage | ✅ |
PATCH | /members/:id | members.manage | ✅ |
POST /members supports two modes: with uid (adds an existing user, returns 200) or with email without uid (creates an invitation, returns 201).
Pagination
Lists use a cursor-based pagination system.
pageSize: number of items per page (default 25, max 100).pageToken: cursor returned asnextPageTokenby the previous response.
async function fetchAllProjects(token: string, portfolioId: string) {
const projects = [];
let pageToken: string | null = null;
do {
const url = new URL("https://kairoproject.app/api/public/v1/projects");
url.searchParams.set("portfolioId", portfolioId);
url.searchParams.set("pageSize", "100");
if (pageToken) url.searchParams.set("pageToken", pageToken);
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
const data = await res.json();
projects.push(...data.items);
pageToken = data.nextPageToken;
} while (pageToken);
return projects;
}
Outbound webhooks
KairoProject can notify your systems in real time via HTTP webhooks.
Configure a webhook
POST /api/public/v1/webhooks
Required scope: webhooks.manage.
Each webhook associates:
- an HTTPS destination URL;
- a list of events to subscribe to;
- an automatically generated signing secret.
Available events
| Event | Trigger |
|---|---|
project.created | Project creation |
project.updated | Project update (includes progress, bufferConsumption) |
project.deleted | Project deletion |
task.created | Task creation |
task.updated | Task update |
task.deleted | Task deletion |
planning.recompute.completed | Planning recompute finished — includes bufferConsumption, progress, status |
planning.buffer_overflow | bufferConsumption >= 1.0 — includes threshold and exceededAt |
Verify the signature
Each delivery includes the header:
X-Kairo-Signature: t=<unix>,v1=<digest>
The digest is computed as: HMAC-SHA256(secret, "${t}.${body}").
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyKairoSignature(
signingSecret: string,
signatureHeader: string,
rawBody: string
): boolean {
const match = signatureHeader.match(/t=([^,]+),v1=([a-f0-9]+)/);
if (!match) return false;
const [, timestamp, signature] = match;
const payload = `${timestamp}.${rawBody}`;
const expected = createHmac("sha256", signingSecret).update(payload).digest("hex");
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(signature, "hex"));
}
Discard deliveries with a timestamp older than 5 minutes to protect against replay attacks.
Response time and retries
Your endpoint must respond within 10 seconds with a 2xx code. On failure, the platform automatically retries up to 5 times: +2 min, +8 min, +30 min, +2h. After that, the event is permanently marked failed. Use event.id to deduplicate multiple deliveries.
Rotate the webhook secret
Use POST /api/public/v1/webhooks/{id}/rotate-secret to renew the signing secret for a webhook. The old value is revoked as soon as rotation occurs.
Error codes
All errors return a uniform JSON body: { "error": "Explicit description of the problem." }
| Code | Common reason |
|---|---|
400 | Validation failed — missing field, invalid format, missing portfolioId |
401 | Token missing, invalid, expired, or client revoked |
403 | Valid token but insufficient scope |
404 | Resource not found in your organisation |
422 | Semantically impossible operation (e.g. deleting a root task) |
429 | Rate limit exceeded |
500 | Unexpected server error — retry after a few seconds |