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 (keyset-paginated, up to 500 per page) |
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 |
PATCH | /api/projects/{projectId}/issues/{issueId} | Update an issue partially |
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/backlog | List the issues in planned and active sprints plus the open backlog (keyset-paginated, 500 per page) |
GET | /api/projects/{projectId}/issues/board | List the issues in the active sprint (capped at 500, not paginated) |
GET | /api/projects/{projectId}/issues/epic-issues/{epicId} | List issues assigned to one epic (keyset-paginated, 500 per page) |
GET | /api/projects/{projectId}/issues/overview | List every issue in a project (keyset-paginated, up to 500 per page) |
GET | /api/projects/{projectId}/issues/search | Search issues (within project): several values per filter, exclusions |
GET | /api/projects/{projectId}/issues/count?group_by= | Count issues per lane, assignee, sprint, epic, priority, type or label |
GET | /api/issues/search | Search issues (cross-project) |
GET | /api/issues/my | List issues assigned to the authenticated user (keyset-paginated, up to 500 per page) |
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,
"lane": { "id": 1, "title": "In Progress" },
"sprint_id": null,
"sprint": null,
"epic_id": null,
"epic": null,
"comment_ids": [],
"priority": 2,
"type": 0,
"estimated_minutes": 240,
"blocked_by_ids": [],
"blocked_by": [],
"blocks_ids": [],
"blocks": [],
"branch_links": [],
"label_ids": [],
"labels": [],
"created_at": "2026-03-13T10:30:00.000000Z",
"token_total": 0,
"open_blockers": []
}Resolved relations
Every issue payload carries its lane, sprint, epic, labels and blocking relations already resolved to names, alongside the ID fields those names come from. Scripts don't need a second call to render an issue.
There is no flag to request them — the fields are always present.
| Field | Type | Meaning |
|---|---|---|
lane | object | { id, title }. Always present; every issue belongs to a lane. |
sprint | object | null | { id, title }, or null when the issue is not in a sprint. |
epic | object | null | { id, title }, or null when the issue has no epic. |
labels | object[] | { id, name, color } per label. [] when none — never null. |
blocked_by | object[] | { id, key, title, lane_id } per issue blocking this one. [] when none. |
blocks | object[] | { id, key, title, lane_id } per issue this one blocks. [] when none. |
open_blockers | object[] | { id, key, title, lane_id } per blocker that is not in the project's final lane, at most 10, lowest id first. unresolved_blockers_count is the total. [] when none. |
The matching lane_id, sprint_id, epic_id, label_ids, blocked_by_ids and blocks_ids fields are unchanged and still populated, so existing consumers keep working.
color on a label is the integer colour value, matching label_ids-era clients and the labels endpoint.
json
{
"lane": { "id": 3, "title": "In Review" },
"sprint": { "id": 12, "title": "Sprint 42" },
"epic": { "id": 4, "title": "CLI parity" },
"labels": [{ "id": 7, "name": "cli", "color": 5 }],
"blocked_by": [{ "id": 38, "key": "KD-0038", "title": "Ship the token endpoint", "lane_id": 2 }],
"blocks": [{ "id": 51, "key": "KD-0051", "title": "Document the payload", "lane_id": 1 }],
"open_blockers": [{ "id": 38, "key": "KD-0038", "title": "Ship the token endpoint", "lane_id": 2 }]
}These nested objects are deliberately minimal — they carry identity and a display name, nothing more. Read the sprints, epics or labels endpoints — or GET /api/projects/{id}/lanes — when you need the full record.
AI token field
One read-only field reports the AI token usage recorded against an issue. It is derived at read time — there is no stored column, and it is not 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_total is a floor: some AI work isn't counted — only sessions that report their usage to kendo are included. The Token Usage guide shows how a session reports it. Don't present the figure as a complete record of AI spend.
Not every issue-returning endpoint carries it. Global Search and My Issues use a leaner cross-project resource that omits the field; Project Search returns the full resource and includes it. Every list endpoint reports meta.truncated, but the flag means one of two things, depending on whether the endpoint pages:
| Endpoint | token_total | Paging | meta.truncated: true means |
|---|---|---|---|
| Get Issue | yes | n/a — single object | n/a |
| List Issues | yes | cursor | another page exists |
| Backlog, Overview, Epic Issues | yes | cursor | another page exists |
| Board, Blocking Dependencies | yes | none — capped at 500 | rows were dropped and cannot be reached |
| Project Search | yes | none — capped at limit | more issues matched; narrow the filters |
| Global Search | no | none — capped at limit | more issues matched; narrow the filters |
| My Issues | no | cursor | another page exists |
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 what the UI accounts for:
- Paged collections. A page carries at most
meta.limitissues and setsmeta.truncatedwhen another page exists, so a sum over one page is a floor, not a total. List Issues, My Issues, Backlog, Overview and Epic Issues all page — followmeta.next_cursorto the last page and the sum covers the whole scope. Board and Blocking Dependencies do not page: they stop at 500 and the rows past that cannot be reached. The UI renders a sum it knows is short asat least N tokens.
Update Issue
PUT /api/projects/{projectId}/issues/{issueId}
Partial update: send PATCH instead of PUT to change a subset of fields. An omitted field keeps its stored value; a field sent as null clears it (assignee_id, sprint_id, epic_id, estimated_minutes, prompt, project_github_repo_id). Omitted label_ids, blocked_by_ids and blocks_ids leave those relations untouched, [] clears them. Every other validation rule is unchanged. A PATCH that sends none of the writable fields is rejected with 422.
Accepts the same fields as create. A PUT must include every required field; a PATCH may omit any of them.
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
}Get Issue
GET /api/projects/{projectId}/issues/{issueId}
{issueId} also takes the issue key, such as KD-0042.
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
}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 in ascending id order, in the same {data, meta} envelope as /api/issues/search. This endpoint is keyset-paginated: a page carries at most meta.limit issues, and meta.next_cursor addresses the page after it. Follow the cursor until it is null to read every issue in a project of any size.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Issues per page (1-500, default 500). A value outside that range returns 422 |
cursor | string | No | Opaque position from the previous page's meta.next_cursor. Absent means the first page; a value that does not decode to an issue id returns 422 |
updated_since | string | No | Return only issues whose updated_at is at or after this moment. ISO-8601 (2026-09-06T12:00:00Z); a value with no UTC offset is read as UTC |
Change of behaviour
limit used to be accepted and ignored, so ?limit=1000 returned a full 500-issue page. It is now validated: ?limit=1000 returns 422. An unparameterised request is unaffected — the default is still 500.
Response Meta
| Field | Type | Description |
|---|---|---|
truncated | boolean | true when a further page exists — always the same answer as next_cursor !== null |
count | integer | Number of issues in this page's data |
limit | integer | Page size applied to this request |
next_cursor | string | null | Position of the next page, or null on the last one. Present on every response from a paged endpoint; the board and blocking lists omit the key entirely, because they do not paginate |
total | integer | Issues in the whole scope, for a "showing N of M" count. Present only where the endpoint counts its scope: the backlog, overview, epic-issues and My Issues listings supply it; this endpoint and the unpaged board and blocking lists omit the key entirely |
Ordering is by id, not by updated_at, so a cursor addresses a position no concurrent edit can move. An issue edited part-way through a walk keeps its place in the order, and a client mirroring the project never reads a live issue as missing. The tab listings and My Issues walk the same key in the opposite direction — newest id first — for the same reason.
Paging Through a Project
bash
cursor=""
while :; do
page=$(curl -s "https://{tenant}.kendo.dev/api/projects/1/issues?limit=500${cursor:+&cursor=$cursor}" \
-H "Authorization: Bearer your-token")
echo "$page" | jq -c '.data[]'
cursor=$(echo "$page" | jq -r '.meta.next_cursor // empty')
[ -z "$cursor" ] || continue
break
doneA cursor past the last issue is not an error: the response is data: [] with next_cursor: null.
Incremental Sync
Walk the whole project once, then fetch only what changed:
- Record the time you started the first walk.
- Walk every page until
next_cursorisnull. - On the next run, pass
updated_sinceset to the previous run's start time and walk again.
Take the timestamp before the walk, not after: an issue edited while the walk was running is then picked up by the following run rather than missed between them. updated_since applies on every page of a walk, so it combines with cursor and limit.
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,
"next_cursor": null
}
}Project Tab Listings
Four endpoints return the issue lists behind a project's Backlog, Board, Overview and Epics screens. They share one response shape: a {data, meta} envelope whose data holds the lean list profile of an issue, not the full resource. Each item carries id, key, title, user_id, assignee_id, project_id, lane_id, sprint_id, epic_id, priority, type, created_at, rank, estimated_minutes, logged_minutes, remaining_minutes, token_total, branch_links, label_ids, unresolved_blockers_count and open_blockers (the first 10 open blockers, shaped as in Resolved relations). It leaves out description, prompt and the other resolved relations; fetch Get Issue for those.
Backlog, Overview and Epic Issues are keyset-paginated the same way: newest id first, and meta.next_cursor addresses the next page. Follow the cursor until it is null to read the whole scope. meta carries truncated, count, limit, next_cursor and total — see Response Meta for each field. Board does not page.
The paged walk is the same loop as Paging Through a Project, pointed at the tab's path.
Backlog
GET /api/projects/{projectId}/issues/backlog
Returns the issues in the project's planned and active sprints, plus every issue without a sprint. An issue without a sprint that sits in the project's final lane (typically Done) is finished work and is left out — unless the project has only one lane.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
cursor | string | No | Opaque position from the previous page's meta.next_cursor. Absent means the first page; a value that does not decode to an issue id returns 422 |
The page size is fixed at 500. The endpoint takes no limit.
bash
curl "https://{tenant}.kendo.dev/api/projects/1/issues/backlog" \
-H "Authorization: Bearer your-token"Board
GET /api/projects/{projectId}/issues/board
Returns the issues in the project's active sprint, most recently updated first. With no active sprint, the response is data: [].
This endpoint is not paginated. It takes no query parameters and stops at 500 issues. When the sprint holds more, meta.truncated is true and the rest cannot be reached here — walk List Issues and keep the issues whose sprint_id is the active sprint. meta carries truncated, count and limit only; there is no next_cursor and no total.
bash
curl "https://{tenant}.kendo.dev/api/projects/1/issues/board" \
-H "Authorization: Bearer your-token"json
{
"data": [
{
"id": 42,
"key": "KD-42",
"title": "Add pagination to issues list",
"lane_id": 2,
"sprint_id": 5,
"token_total": 0,
"unresolved_blockers_count": 0,
"open_blockers": [],
"...": "..."
}
],
"meta": {
"truncated": false,
"count": 1,
"limit": 500
}
}Overview
GET /api/projects/{projectId}/issues/overview
Returns every issue in the project, in every lane and sprint. meta.total counts the whole project.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Issues per page (1-500, default 500). A value outside that range returns 422 |
cursor | string | No | Opaque position from the previous page's meta.next_cursor. Absent means the first page; a value that does not decode to an issue id returns 422 |
bash
curl "https://{tenant}.kendo.dev/api/projects/1/issues/overview?limit=2" \
-H "Authorization: Bearer your-token"json
{
"data": [
{ "id": 43, "key": "KD-43", "title": "Fix email validation on login form", "...": "..." },
{ "id": 42, "key": "KD-42", "title": "Add pagination to issues list", "...": "..." }
],
"meta": {
"truncated": true,
"count": 2,
"limit": 2,
"next_cursor": "eyJpZCI6NDIsIl9wb2ludHNUb05leHRJdGVtcyI6dHJ1ZX0",
"total": 37
}
}Epic Issues
GET /api/projects/{projectId}/issues/epic-issues/{epicId}
Returns the issues assigned to one epic, across every sprint status — an issue in a completed sprint is included. An epic that belongs to another project returns 404.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
cursor | string | No | Opaque position from the previous page's meta.next_cursor. Absent means the first page; a value that does not decode to an issue id returns 422 |
The page size is fixed at 500. The endpoint takes no limit.
bash
curl "https://{tenant}.kendo.dev/api/projects/1/issues/epic-issues/7" \
-H "Authorization: Bearer your-token"Project Search
GET /api/projects/{projectId}/issues/search
Search and filter the issues of one project. Every filter is optional. Filters on different fields combine with AND; the values inside one list combine with OR.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | No | Text search across title, description, and key (max 200 chars) |
lane_ids[] | integer[] | No | Issues in any of these lanes. Every ID must belong to the project |
assignee_ids[] | integer[] | No | Issues assigned to any of these users. 0 means unassigned |
sprint_ids[] | integer[] | No | Issues in any of these sprints. 0 means no sprint (backlog). Every other ID must belong to the project |
epic_ids[] | integer[] | No | Issues in any of these epics. 0 means no epic. Every other ID must belong to the project |
priorities[] | integer[] | No | Issues with any of these priorities (0-4, see enums below) |
types[] | integer[] | No | Issues of any of these types (0-2, see enums below) |
label_id[] | integer[] | No | Issues carrying any of these labels. Every ID must belong to the project; a repeated ID is rejected |
exclude_lane_ids[] | integer[] | No | Leave out issues in these lanes |
exclude_assignee_ids[] | integer[] | No | Leave out issues assigned to these users |
exclude_sprint_ids[] | integer[] | No | Leave out issues in these sprints |
exclude_epic_ids[] | integer[] | No | Leave out issues in these epics |
exclude_label_ids[] | integer[] | No | Leave out issues carrying any of these labels |
exclude_priorities[] | integer[] | No | Leave out issues with these priorities |
exclude_types[] | integer[] | No | Leave out issues of these types |
lane_id, assignee_id, sprint_id, epic_id, priority, type | integer | No | One value for the same filter. The server adds it to the matching list, so lane_id=3 equals lane_ids[]=3. assignee_id, sprint_id and epic_id do not accept 0; send 0 in the list form |
sprint_status[] | integer[] | No | Issues whose sprint has one of these statuses (0 Planned, 1 Active, 2 Completed) |
include_backlog | boolean | No | With sprint_status[]: also return issues without a sprint. Alone: return only issues without a sprint |
since | string | No | Issues created on or after this date (YYYY-MM-DD) |
limit | integer | No | Max results to return (1-500, default 25) |
Every ID list holds at most 100 IDs. A lane, sprint, epic or label ID that does not belong to the project fails validation with 422, in include and exclude lists alike. So does a user ID that does not exist.
The 0 value and exclusions
- In
assignee_ids[],sprint_ids[]andepic_ids[],0selects issues without a value.assignee_ids[]=0&assignee_ids[]=12returns issues that are unassigned or assigned to user 12. - An exclusion keeps issues without a value.
exclude_assignee_ids[]=12still returns unassigned issues. Add0to the exclude list to drop them too:exclude_epic_ids[]=0returns only issues that have an epic. - When the same value is in an include list and in the exclude list for that field, the exclusion wins.
Response Shape
The response is a {data, meta} envelope with full issue objects in data, ordered by most recently updated. meta carries truncated, count and limit. meta.truncated is true when more issues match than meta.limit.
bash
curl -G "https://{tenant}.kendo.dev/api/projects/1/issues/search" \
--data-urlencode "lane_ids[]=1" --data-urlencode "lane_ids[]=2" \
--data-urlencode "exclude_types[]=2" \
--data-urlencode "assignee_ids[]=0" \
-H "Authorization: Bearer {token}"Count Issues
GET /api/projects/{projectId}/issues/count
Count the issues of one project per value of one field. The endpoint accepts every filter of Project Search except limit, with the same rules, so a search and a count with the same filters describe the same issues. The count is not capped.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
group_by | string | Yes | lane, assignee, sprint, epic, priority, type or label |
| Project Search filters | No | See Project Search |
Response Shape
The body is not wrapped in data.
| Field | Type | Description |
|---|---|---|
total | integer | Number of issues that match the filters |
groups | object[] | One entry per value that has at least one issue, highest count first |
groups[].key | integer | Lane, user, sprint, epic or label ID; for priority and type the enum number. 0 is the group without a value for assignee (Unassigned), sprint (Backlog), epic (No epic) and label (No label). For priority and type, 0 is the real value (Highest, Feature) |
groups[].label | string | Lane title, user name, sprint title, epic title, label name, or the priority / type name |
groups[].count | integer | Number of matching issues in the group |
With group_by=label an issue with several labels is counted in each of its label groups, so the group counts can add up to more than total. total stays the number of distinct issues. For every other group_by, the group counts add up to total.
bash
curl -G "https://{tenant}.kendo.dev/api/projects/1/issues/count" \
--data-urlencode "group_by=lane" \
--data-urlencode "exclude_types[]=2" \
-H "Authorization: Bearer {token}"json
{
"total": 12,
"groups": [
{ "key": 1, "label": "To Do", "count": 7 },
{ "key": 2, "label": "In Progress", "count": 5 }
]
}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 the same {data, meta} envelope as /api/issues/search.
This endpoint is keyset-paginated: a page carries at most meta.limit issues, newest id first, and meta.next_cursor addresses the page after it. Follow the cursor until it is null to read every assigned issue. meta.total is the size of the whole scope, so a client can render "showing N of M".
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
cursor | string | No | Opaque position from the previous page's meta.next_cursor. Absent means the first page; a value that does not decode to an issue id returns 422 |
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,
"next_cursor": null,
"total": 1
}
}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