Appearance
Projects API
Projects are the top-level container for issues, sprints, epics, and board lanes.
Permissions
| Action | Required Permission | Scope |
|---|---|---|
| List | Projects: Read | Only projects accessible via team membership, accessAllProjects role flag, or ownership |
| View | Projects: Read | Must have project access |
| Create | Projects: Create | — |
| Update | Projects: Update (Own/All) | Must have project access |
| Archive / restore | Projects: Update (Own/All) | Must have project access |
| Delete | Projects: Delete (Own/All) | Must have project access |
| Transfer ownership | Owner or Admin | Must be project owner or admin |
| Upload / remove avatar | Projects: Update (Own/All) | Must have project access |
Admins bypass all permission checks. Project owners have full access to their own projects.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/projects | List projects — active only unless include_archived is set |
POST | /api/projects | Create a project |
GET | /api/projects/limit-status | Check the tenant's project count against its plan limit |
GET | /api/projects/{id} | Get a project |
PUT | /api/projects/{id} | Update a project |
DELETE | /api/projects/{id} | Delete a project |
POST | /api/projects/{id}/avatar | Upload a project avatar |
DELETE | /api/projects/{id}/avatar | Remove the project avatar |
POST | /api/projects/{id}/archive | Archive the project |
POST | /api/projects/{id}/restore | Restore an archived project |
POST | /api/projects/{id}/pin | Pin or unpin the project for the current user |
Create Project
POST /api/projects
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Project name, max 255 characters |
code | string | Yes | 2-5 uppercase letters (e.g., KD), must be unique |
description | string | Yes | Project description, max 5,000 characters |
user_id | integer | Yes | Project owner user ID |
team_ids | integer[] | Yes | Team IDs to grant access (can be empty []) |
direct_member_ids | integer[] | Yes | Individual user IDs to grant access (can be empty []) |
bash
curl -X POST https://{tenant}.kendo.dev/api/projects \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{
"name": "Website Redesign",
"code": "WEB",
"description": "Complete redesign of the public website with new branding.",
"user_id": 1,
"team_ids": [1],
"direct_member_ids": [4, 5]
}'json
{
"id": 3,
"name": "Website Redesign",
"code": "WEB",
"description": "Complete redesign of the public website with new branding.",
"user_id": 1,
"issue_ids": [],
"lane_ids": [10, 11, 12, 13],
"team_ids": [1],
"direct_member_ids": [4, 5],
"created_at": "2026-03-13T10:30:00.000000Z",
"avatar": null
}Project Limit Status
GET /api/projects/limit-status
Reports the tenant's current project count against its plan limit, so a client can warn the user before they fill in the create-project form. Requires Projects: Create — the same permission that gates POST /api/projects — rather than billing-management access, so any user who can create a project can also check the limit. Free-plan limits are enforced regardless of this endpoint: POST /api/projects still returns 402 Payment Required if the tenant is over its limit at submit time. This endpoint is UX only, not authorization.
Paid (subscribed) tenants get an effectively unlimited project_limit.
bash
curl https://{tenant}.kendo.dev/api/projects/limit-status \
-H "Authorization: Bearer your-token"json
{
"project_limit": 1,
"project_count": 1
}Update Project
PUT /api/projects/{id}
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Project name, max 255 characters |
code | string | Yes | 2-5 uppercase letters, unique (except self) |
description | string | Yes | Max 5,000 characters |
user_id | integer | Yes | Project owner user ID |
lane_ids | integer[] | Yes | Ordered lane IDs (min 1, must belong to project) |
team_ids | integer[] | Yes | Team IDs (can be empty []) |
direct_member_ids | integer[] | Yes | Individual user IDs (can be empty []) |
bash
curl -X PUT https://{tenant}.kendo.dev/api/projects/3 \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{
"name": "Website Redesign",
"code": "WEB",
"description": "Complete redesign of the public website with new branding.",
"user_id": 1,
"lane_ids": [10, 11, 12, 13],
"team_ids": [1, 2],
"direct_member_ids": []
}'json
{
"id": 3,
"name": "Website Redesign",
"code": "WEB",
"description": "Complete redesign of the public website with new branding.",
"user_id": 1,
"issue_ids": [50, 51, 52],
"lane_ids": [10, 11, 12, 13],
"team_ids": [1, 2],
"direct_member_ids": [],
"created_at": "2026-03-13T10:30:00.000000Z",
"avatar": null
}Delete Project
DELETE /api/projects/{id}
Returns 204 No Content on success.
bash
curl -X DELETE https://{tenant}.kendo.dev/api/projects/3 \
-H "Authorization: Bearer your-token"WARNING
Deleting a project removes all its issues, sprints, epics, lanes, and time entries. This action cannot be undone.
Archive / Restore Project
POST /api/projects/{id}/archive · POST /api/projects/{id}/restore
Archiving takes a finished project out of the default project list without deleting anything. The project keeps every issue, sprint, epic, lane and time entry, stays reachable at GET /api/projects/{id}, and its issues keep appearing in search and in time-entry exports. Both endpoints require Projects: Update and return the updated project resource.
Archiving is workspace-wide, not a per-user preference: once archived, the project is out of the list for everyone in the tenant. Restoring puts it back for everyone, with pins intact.
Every project resource carries two fields for this:
| Field | Type | Description |
|---|---|---|
archived_at | string | null | ISO timestamp of the moment the project was archived, null while active |
archived | boolean | Derived from archived_at — never stored separately, so the two cannot disagree |
bash
curl -X POST https://{tenant}.kendo.dev/api/projects/3/archive \
-H "Authorization: Bearer your-token"json
{
"id": 3,
"name": "Website Redesign",
"code": "WEB",
"archived_at": "2026-08-03T11:34:00.000000Z",
"archived": true
}Both calls are idempotent: archiving an already-archived project leaves archived_at as it was, and restoring an active project is a no-op.
Listing archived projects
GET /api/projects returns active projects only. Pass include_archived to get active and archived in one list:
bash
curl "https://{tenant}.kendo.dev/api/projects?include_archived=1" \
-H "Authorization: Bearer your-token"Any value the parameter does not recognise is treated as false, so a malformed request hides archived projects rather than exposing them unexpectedly. There is no archived-only variant of this endpoint — filter on archived client-side.
Endpoints that address a single project (GET /api/projects/{id}, and everything nested under it) never apply this filter; an archived project is fully readable and writable by id.
Project Avatar
A project can have an uploaded avatar image. On upload, the image is re-encoded into two optimised variants — AVIF and WebP — and its EXIF metadata is stripped. When a project has no avatar, the avatar field is null; clients should fall back to a generated placeholder.
The avatar field on the project resource is either null or an object of absolute CDN URLs to the two variants:
json
"avatar": {
"avif": "https://cdn.kendo.dev/k7fq2p.../project-avatars/1ChqqBvEsCTjLG8G.avif",
"webp": "https://cdn.kendo.dev/k7fq2p.../project-avatars/1ChqqBvEsCTjLG8G.webp"
}These URLs are served straight from the CDN, so fetching one needs no Authorization header. Each is cached for a year and the filename changes on every upload, so a replaced avatar is always a new URL.
Upload Avatar
POST /api/projects/{id}/avatar
Send the image as multipart/form-data. Requires Projects: Update.
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | Image file — JPEG, PNG, WebP, or AVIF, max 10 MB |
Returns the updated project resource with the avatar field populated. Re-uploading replaces the previous image.
bash
curl -X POST https://{tenant}.kendo.dev/api/projects/3/avatar \
-H "Authorization: Bearer your-token" \
-F "[email protected]"json
{
"id": 3,
"name": "Website Redesign",
"code": "WEB",
"description": "Complete redesign of the public website with new branding.",
"user_id": 1,
"issue_ids": [50, 51, 52],
"lane_ids": [10, 11, 12, 13],
"team_ids": [1, 2],
"direct_member_ids": [],
"created_at": "2026-03-13T10:30:00.000000Z",
"avatar": {
"avif": "https://cdn.kendo.dev/k7fq2p.../project-avatars/1ChqqBvEsCTjLG8G.avif",
"webp": "https://cdn.kendo.dev/k7fq2p.../project-avatars/1ChqqBvEsCTjLG8G.webp"
}
}Fetch Avatar
Use either URL from the avatar object directly — there is no API endpoint for the image bytes.
bash
curl https://cdn.kendo.dev/k7fq2p.../project-avatars/1ChqqBvEsCTjLG8G.webp \
--output avatar.webpYou normally won't call this directly — use the URLs from the project resource's avatar field, which already point at this endpoint.
Remove Avatar
DELETE /api/projects/{id}/avatar
Deletes the stored image and its variants. Requires Projects: Update. Returns the updated project resource with avatar set to null.
bash
curl -X DELETE https://{tenant}.kendo.dev/api/projects/3/avatar \
-H "Authorization: Bearer your-token"Pin / Unpin Project
POST /api/projects/{id}/pin
Toggles the project as "pinned" for the authenticated user, personalizing their own sidebar — it does not affect any other user's view of the project. Requires the same access as viewing the project. Calling this endpoint again on an already-pinned project unpins it.
The response is your own profile, with pinned_project_ids reflecting the new state — not the project resource.
bash
curl -X POST https://{tenant}.kendo.dev/api/projects/3/pin \
-H "Authorization: Bearer your-token"json
{
"id": 1,
"first_name": "Ada",
"last_name": "Lovelace",
"pinned_project_ids": [3]
}See Also
- Issues API — Manage issues within a project
- Sprints API — Sprint management
- Epics API — Epic management
- Projects guide — Working with projects in the UI