Appearance
Issues API
Issues are the core work items in kendo. Each issue belongs to a project and has a unique key (e.g., KD-42).
Permissions
| Action | Required Permission | Scope |
|---|---|---|
| List / search / view | Issues: Read | Issues within accessible projects |
| Create | Issues: Create | In accessible projects |
| Update | Issues: Update (Own/All) | Own: only issues you created. All: any issue in accessible projects |
| Delete | Issues: Delete (Own/All) | Own: only issues you created. All: any issue in accessible projects |
Admins and project owners bypass all permission checks for project-scoped resources.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/projects/{projectId}/issues | List issues (capped at 500) |
POST | /api/projects/{projectId}/issues | Create an issue |
GET | /api/projects/{projectId}/issues/{issueId} | Get an issue |
PUT | /api/projects/{projectId}/issues/{issueId} | Update an issue |
DELETE | /api/projects/{projectId}/issues/{issueId} | Delete an issue |
GET | /api/projects/{projectId}/issues/{issueId}/blocking | Get an issue's blocking dependencies |
GET | /api/projects/{projectId}/issues/search?q= | Search issues (within project) |
GET | /api/issues/search | Search issues (cross-project) |
GET | /api/issues/my | List issues assigned to the authenticated user |
Create Issue
POST /api/projects/{projectId}/issues
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Issue title, max 255 characters |
description | string | Yes | Markdown description, max 65,535 characters |
lane_id | integer | Yes | Board lane ID (must belong to the project) |
priority | integer | Yes | 0 Highest, 1 High, 2 Medium, 3 Low, 4 Lowest |
type | integer | Yes | 0 Feature, 1 Bug |
assignee_id | integer | No | User ID (must be a project member) |
sprint_id | integer | No | Sprint ID (must belong to the project) |
epic_id | integer | No | Epic ID (must belong to the project) |
estimated_minutes | integer | No | Time estimate in minutes, min 0 |
blocked_by_ids | integer[] | No | Issue IDs that block this issue |
blocks_ids | integer[] | No | Issue IDs that this issue blocks |
label_ids | integer[] | No | Label IDs to attach (must belong to the project) |
prompt | string | No | AI prompt context, max 10,000 characters |
bash
curl -X POST https://{tenant}.kendo.dev/api/projects/1/issues \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{
"title": "Add pagination to issues list",
"description": "The issues overview needs cursor-based pagination for large projects.",
"lane_id": 1,
"priority": 2,
"type": 0,
"assignee_id": 3,
"estimated_minutes": 240
}'json
{
"id": 42,
"key": "KD-42",
"title": "Add pagination to issues list",
"description": "The issues overview needs cursor-based pagination for large projects.",
"prompt": null,
"user_id": 1,
"assignee_id": 3,
"project_id": 1,
"lane_id": 1,
"sprint_id": null,
"epic_id": null,
"comment_ids": [],
"priority": 2,
"type": 0,
"estimated_minutes": 240,
"blocked_by_ids": [],
"blocks_ids": [],
"branch_links": [],
"label_ids": [],
"created_at": "2026-03-13T10:30:00.000000Z",
"token_total": 0,
"token_capture_complete": true,
"handed_to_claude_at": null
}AI token fields
Two read-only fields report the AI token usage recorded against an issue. Both are derived at read time — there is no stored column, and neither is accepted on create or update.
| Field | Type | Meaning |
|---|---|---|
token_total | integer | Sum of every recorded token category across the issue's token events. 0 when none — zero means no AI work was logged, not that the value is unknown. |
token_capture_complete | boolean | false when at least one Claude session on the issue was not fully captured, meaning token_total understates the real spend. Treat the number as a lower bound and present it as such. |
token_total is a floor even when token_capture_complete is true: some platform-side usage is not reported to us at all, so no issue's total includes it. Don't present the figure as a complete record of AI spend.
Not every issue-returning endpoint carries them. The search-shaped responses use a leaner resource that omits both fields, and only the capped endpoints report a meta.truncated flag:
| Endpoint | Token fields | meta.truncated |
|---|---|---|
| Get Issue | yes | n/a — single object |
| List Issues | yes | yes |
| Search Issues, and the board / backlog / recent / epic-issues / blocking tabs | yes | yes |
| Global Search | no | yes |
| My Issues | no | yes |
There is no sprint- or epic-level rollup endpoint. Sprint and epic totals are not fields on those resources — kendo's own UI sums token_total across the issues it has already loaded. If you aggregate the same way, mind the two things the UI accounts for:
- Capped collections. The capped endpoints stop at 500 issues and set
meta.truncated; a sum over a truncated page is a floor, not a total. - Incomplete capture. Any issue with
token_capture_complete: falsemakes the whole sum a floor too. The UI renders both cases identically, asat least N tokens.
Update Issue
PUT /api/projects/{projectId}/issues/{issueId}
Accepts the same fields as create. All required fields must be included in every update.
bash
curl -X PUT https://{tenant}.kendo.dev/api/projects/1/issues/42 \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{
"title": "Add pagination to issues list",
"description": "The issues overview needs cursor-based pagination for large projects.",
"lane_id": 2,
"priority": 1,
"type": 0,
"sprint_id": 5,
"blocked_by_ids": [38, 40],
"label_ids": [1, 2]
}'json
{
"id": 42,
"key": "KD-42",
"title": "Add pagination to issues list",
"description": "The issues overview needs cursor-based pagination for large projects.",
"prompt": null,
"user_id": 1,
"assignee_id": 3,
"project_id": 1,
"lane_id": 2,
"sprint_id": 5,
"epic_id": null,
"comment_ids": [],
"priority": 1,
"type": 0,
"estimated_minutes": 240,
"blocked_by_ids": [38, 40],
"blocks_ids": [],
"branch_links": [],
"label_ids": [],
"created_at": "2026-03-13T10:30:00.000000Z",
"token_total": 0,
"token_capture_complete": true,
"handed_to_claude_at": null
}Get Issue
GET /api/projects/{projectId}/issues/{issueId}
bash
curl https://{tenant}.kendo.dev/api/projects/1/issues/42 \
-H "Authorization: Bearer your-token"json
{
"id": 42,
"key": "KD-42",
"title": "Add pagination to issues list",
"description": "The issues overview needs cursor-based pagination for large projects.",
"prompt": null,
"user_id": 1,
"assignee_id": 3,
"project_id": 1,
"lane_id": 2,
"sprint_id": 5,
"epic_id": null,
"comment_ids": [10, 11],
"priority": 1,
"type": 0,
"estimated_minutes": 240,
"blocked_by_ids": [38, 40],
"blocks_ids": [],
"branch_links": [
{
"id": 7,
"issue_id": 42,
"branch_name": "feat/pagination",
"branch_url": "https://github.com/acme/widgets/tree/feat/pagination",
"status": 1,
"created_by": 3,
"created_at": "2026-03-13T10:45:00.000000Z",
"creator": {"id": 3, "first_name": "Sam", "last_name": "Smith"},
"project_github_repo": {
"id": 4,
"project_id": 1,
"repo_full_name": "acme/widgets",
"repo_url": "https://github.com/acme/widgets",
"default_branch": "main",
"is_primary": true,
"owner": "acme",
"repo_name": "widgets",
"branch_links_count": 3
}
}
],
"label_ids": [1, 4],
"created_at": "2026-03-13T10:30:00.000000Z",
"token_total": 0,
"token_capture_complete": true,
"handed_to_claude_at": "2026-03-13T11:00:00.000000Z"
}Delete Issue
DELETE /api/projects/{projectId}/issues/{issueId}
Returns 204 No Content on success.
bash
curl -X DELETE https://{tenant}.kendo.dev/api/projects/1/issues/42 \
-H "Authorization: Bearer your-token"Blocking Dependencies
GET /api/projects/{projectId}/issues/{issueId}/blocking
Returns the issue's dependency graph neighbours — the union of the issues that block it (blocked_by) and the issues it blocks — as a lean, capped list. The dependencies are resolved directly, with no sprint or lane filter, so a blocker that lives in a completed sprint is still returned (unlike the sprint-scoped board/backlog lists). The endpoint issues no request shaping beyond the cap; it is intended for resolving the small set of dependency cards shown on an issue's detail and edit screens.
{issueId} accepts the numeric id or the issue key (e.g. KD-0042).
Each item is the lean issue shape (id, key, title, lane_id, sprint_id, plus the other list fields) — not the full issue resource. The response uses the shared capped-list envelope:
json
{
"data": [
{"id": 7, "key": "PA-0007", "title": "Migrate auth tables", "lane_id": 3, "sprint_id": 12},
{"id": 9, "key": "PA-0009", "title": "Document rollout", "lane_id": 1, "sprint_id": null}
],
"meta": {"truncated": false, "count": 2, "limit": 500}
}bash
curl https://{tenant}.kendo.dev/api/projects/1/issues/42/blocking \
-H "Authorization: Bearer your-token"List Issues
GET /api/projects/{projectId}/issues
Returns the project's issues, most recently updated first, in the same {data, meta} envelope as /api/issues/search. Capped at 500 issues: when more match, meta.truncated is true and the remainder is not reachable through this endpoint — narrow the set with GET /api/projects/{projectId}/issues/search instead.
bash
curl https://{tenant}.kendo.dev/api/projects/1/issues \
-H "Authorization: Bearer your-token"json
{
"data": [
{
"id": 42,
"key": "KD-42",
"title": "Add pagination to issues list",
"priority": 1,
"type": 0,
"lane_id": 2,
"assignee_id": 3,
"sprint_id": 5,
"...": "..."
},
{
"id": 43,
"key": "KD-43",
"title": "Fix email validation on login form",
"priority": 0,
"type": 1,
"lane_id": 1,
"assignee_id": null,
"sprint_id": null,
"...": "..."
}
],
"meta": {
"truncated": false,
"count": 2,
"limit": 500
}
}Global Search
GET /api/issues/search
Search issues across all accessible projects. Admins can search all projects; non-admin users only see issues from projects they have access to.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | No | Text search across title, description, and key (max 200 chars) |
project_id | integer | No | Filter by project ID |
lane_id | integer | No | Filter by lane ID |
assignee_id | integer | No | Filter by assignee user ID |
sprint_id | integer | No | Filter by sprint ID |
epic_id | integer | No | Filter by epic ID |
priority | integer | No | Filter by priority (0-4, see enums below) |
type | integer | No | Filter by type (0-1, see enums below) |
exclude_final_lane | boolean | No | Exclude issues in each project's highest-order lane (typically Done) |
limit | integer | No | Max results to return (1-500, default 25) |
Response Shape
Search responses use a {data, meta} envelope. The server caps result sets at 500 issues; when a query matches more, meta.truncated is true and callers should refine the filter set to see additional rows.
| Field | Type | Description |
|---|---|---|
data | IssueSearchResource[] | Matching issues (never more than meta.limit) |
meta.truncated | boolean | true when the result set was capped at meta.limit; refine filters to see more matches |
meta.count | integer | Number of issues actually returned in data |
meta.limit | integer | Server-side cap applied to this request |
bash
curl "https://{tenant}.kendo.dev/api/issues/search?query=login&priority=1&limit=10" \
-H "Authorization: Bearer your-token"json
{
"data": [
{
"id": 42,
"key": "KD-42",
"title": "Fix login timeout bug",
"project_id": 1,
"project_name": "Backend",
"lane_id": 2,
"lane_title": "In Progress",
"lane_color": 1,
"epic_id": 7,
"epic_title": "Authentication Overhaul",
"user_id": 2,
"assignee_id": 3,
"priority": 1,
"type": 1,
"updated_at": "2026-03-13T10:30:00+00:00"
}
],
"meta": {
"truncated": false,
"count": 1,
"limit": 10
}
}My Issues
GET /api/issues/my
List all issues assigned to the authenticated user across every project they can access, excluding issues in each project's final lane (typically Done). Returns up to 500 issues in the same {data, meta} envelope as /api/issues/search.
Query Parameters
None. Filtering is client-side.
bash
curl "https://{tenant}.kendo.dev/api/issues/my" \
-H "Authorization: Bearer your-token"json
{
"data": [
{
"id": 42,
"key": "KD-42",
"title": "Fix login timeout bug",
"project_id": 1,
"project_name": "Backend",
"lane_id": 2,
"lane_title": "In Progress",
"lane_color": 1,
"epic_id": 7,
"epic_title": "Authentication Overhaul",
"user_id": 2,
"assignee_id": 3,
"priority": 1,
"type": 1,
"updated_at": "2026-03-13T10:30:00+00:00"
}
],
"meta": {
"truncated": false,
"count": 1,
"limit": 500
}
}AI Story Generation
POST /api/projects/{projectId}/issues/agent-generate-story
Generates an AI-powered issue story using a multi-agent pipeline (Validate → Duplicate Check → Research → Classify → Write). Requires the generateStory permission. Rate-limited via throttle:story.
Real-time progress is broadcast via WebSocket on the user's private channel (Tenant.{tenantId}.App.Models.User.{userId}) as agent-progress events during processing.
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
description | string | Yes | The input text describing the desired issue |
context | string | No | Additional context prepended to the description |
Response Fields
| Field | Type | Description |
|---|---|---|
title | string | AI-generated issue title |
description | string | AI-generated issue description (Markdown) |
type | integer | Suggested issue type (0 Feature, 1 Bug) |
priority | integer | Suggested priority (0-4) |
intent | string | Pipeline result intent: create, update, or respond |
reasoning | string|null | Explanation when intent is update or respond |
issue_id | integer|null | Existing issue ID when intent is update |
pipeline | array|null | Pipeline step summary (see below), null when empty |
Pipeline Step Object
Each entry in the pipeline array describes the result of one agent step:
| Field | Type | Description |
|---|---|---|
step | string | Agent name: validate, duplicate_check, research, classify, write |
status | integer | 0 Success, 1 Warning, 2 Skipped |
message | string | Human-readable result message (English) |
bash
curl -X POST https://{tenant}.kendo.dev/api/projects/1/issues/agent-generate-story \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{
"description": "Users can not reset their password when 2FA is enabled"
}'json
{
"title": "Fix password reset flow when 2FA is enabled",
"description": "## Problem\n\nUsers with two-factor authentication enabled cannot complete the password reset flow...",
"type": 1,
"priority": 1,
"intent": "create",
"reasoning": null,
"issue_id": null,
"pipeline": [
{"step": "validate", "status": 0, "message": "Input is actionable"},
{"step": "duplicate_check", "status": 0, "message": "No duplicates found"},
{"step": "research", "status": 0, "message": "Found 3 relevant issues"},
{"step": "classify", "status": 0, "message": "Classified as new bug report"},
{"step": "write", "status": 0, "message": "Story generated successfully"}
]
}WebSocket Progress Events
During generation, agent-progress events are broadcast on the user's private channel:
| Field | Type | Description |
|---|---|---|
step | string | Agent name (same as pipeline step names) |
phase | integer | 0 Pending, 1 Started, 2 Completed |
message | string | Human-readable progress message (English) |
stepIndex | integer | Zero-based step position (0-4) |
totalSteps | integer | Total steps in the pipeline (5) |
Events are best-effort — the pipeline works correctly even when WebSocket (Reverb) is unavailable. The HTTP response pipeline field is the authoritative result.
Enums
Priority
| Value | Label |
|---|---|
0 | Highest |
1 | High |
2 | Medium |
3 | Low |
4 | Lowest |
Type
| Value | Label |
|---|---|
0 | Feature |
1 | Bug |
Response Field Notes
The label_ids field is a flat list of label IDs currently attached to the issue. Fetch the project's labels via GET /api/projects/{projectId}/labels once and join client-side — labels are stable per-project taxonomy. Manage assignments via PUT /api/projects/{projectId}/issues/{issueId}/labels.
See Also
- Labels API — Manage the project's label taxonomy and assign labels to issues
- Comments API — Add comments to issues
- Time Entries API — Log time against issues
- Sprints API — Assign issues to sprints
- Issues & Board guide — Working with issues in the UI