Skip to content

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 ​

ActionRequired PermissionScope
List / search / viewIssues: ReadIssues within accessible projects
CreateIssues: CreateIn accessible projects
UpdateIssues: Update (Own/All)Own: only issues you created. All: any issue in accessible projects
DeleteIssues: 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 ​

MethodEndpointDescription
GET/api/projects/{projectId}/issuesList issues (keyset-paginated, up to 500 per page)
POST/api/projects/{projectId}/issuesCreate 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}/blockingGet an issue's blocking dependencies
GET/api/projects/{projectId}/issues/backlogList the issues in planned and active sprints plus the open backlog (keyset-paginated, 500 per page)
GET/api/projects/{projectId}/issues/boardList 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/overviewList every issue in a project (keyset-paginated, up to 500 per page)
GET/api/projects/{projectId}/issues/searchSearch 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/searchSearch issues (cross-project)
GET/api/issues/myList issues assigned to the authenticated user (keyset-paginated, up to 500 per page)

Create Issue ​

POST /api/projects/{projectId}/issues

Request Fields ​

FieldTypeRequiredDescription
titlestringYesIssue title, max 255 characters
descriptionstringYesMarkdown description, max 65,535 characters
lane_idintegerYesBoard lane ID (must belong to the project)
priorityintegerYes0 Highest, 1 High, 2 Medium, 3 Low, 4 Lowest
typeintegerYes0 Feature, 1 Bug
assignee_idintegerNoUser ID (must be a project member)
sprint_idintegerNoSprint ID (must belong to the project)
epic_idintegerNoEpic ID (must belong to the project)
estimated_minutesintegerNoTime estimate in minutes, min 0
blocked_by_idsinteger[]NoIssue IDs that block this issue
blocks_idsinteger[]NoIssue IDs that this issue blocks
label_idsinteger[]NoLabel IDs to attach (must belong to the project)
promptstringNoAI 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.

FieldTypeMeaning
laneobject{ id, title }. Always present; every issue belongs to a lane.
sprintobject | null{ id, title }, or null when the issue is not in a sprint.
epicobject | null{ id, title }, or null when the issue has no epic.
labelsobject[]{ id, name, color } per label. [] when none — never null.
blocked_byobject[]{ id, key, title, lane_id } per issue blocking this one. [] when none.
blocksobject[]{ id, key, title, lane_id } per issue this one blocks. [] when none.
open_blockersobject[]{ 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.

FieldTypeMeaning
token_totalintegerSum 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:

Endpointtoken_totalPagingmeta.truncated: true means
Get Issueyesn/a — single objectn/a
List Issuesyescursoranother page exists
Backlog, Overview, Epic Issuesyescursoranother page exists
Board, Blocking Dependenciesyesnone — capped at 500rows were dropped and cannot be reached
Project Searchyesnone — capped at limitmore issues matched; narrow the filters
Global Searchnonone — capped at limitmore issues matched; narrow the filters
My Issuesnocursoranother 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.limit issues and sets meta.truncated when 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 — follow meta.next_cursor to 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 as at 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 ​

ParameterTypeRequiredDescription
limitintegerNoIssues per page (1-500, default 500). A value outside that range returns 422
cursorstringNoOpaque 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_sincestringNoReturn 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 ​

FieldTypeDescription
truncatedbooleantrue when a further page exists — always the same answer as next_cursor !== null
countintegerNumber of issues in this page's data
limitintegerPage size applied to this request
next_cursorstring | nullPosition 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
totalintegerIssues 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
done

A 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:

  1. Record the time you started the first walk.
  2. Walk every page until next_cursor is null.
  3. On the next run, pass updated_since set 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 ​

ParameterTypeRequiredDescription
cursorstringNoOpaque 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 ​

ParameterTypeRequiredDescription
limitintegerNoIssues per page (1-500, default 500). A value outside that range returns 422
cursorstringNoOpaque 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 ​

ParameterTypeRequiredDescription
cursorstringNoOpaque 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"

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 ​

ParameterTypeRequiredDescription
querystringNoText search across title, description, and key (max 200 chars)
lane_ids[]integer[]NoIssues in any of these lanes. Every ID must belong to the project
assignee_ids[]integer[]NoIssues assigned to any of these users. 0 means unassigned
sprint_ids[]integer[]NoIssues in any of these sprints. 0 means no sprint (backlog). Every other ID must belong to the project
epic_ids[]integer[]NoIssues in any of these epics. 0 means no epic. Every other ID must belong to the project
priorities[]integer[]NoIssues with any of these priorities (0-4, see enums below)
types[]integer[]NoIssues of any of these types (0-2, see enums below)
label_id[]integer[]NoIssues carrying any of these labels. Every ID must belong to the project; a repeated ID is rejected
exclude_lane_ids[]integer[]NoLeave out issues in these lanes
exclude_assignee_ids[]integer[]NoLeave out issues assigned to these users
exclude_sprint_ids[]integer[]NoLeave out issues in these sprints
exclude_epic_ids[]integer[]NoLeave out issues in these epics
exclude_label_ids[]integer[]NoLeave out issues carrying any of these labels
exclude_priorities[]integer[]NoLeave out issues with these priorities
exclude_types[]integer[]NoLeave out issues of these types
lane_id, assignee_id, sprint_id, epic_id, priority, typeintegerNoOne 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[]NoIssues whose sprint has one of these statuses (0 Planned, 1 Active, 2 Completed)
include_backlogbooleanNoWith sprint_status[]: also return issues without a sprint. Alone: return only issues without a sprint
sincestringNoIssues created on or after this date (YYYY-MM-DD)
limitintegerNoMax 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[] and epic_ids[], 0 selects issues without a value. assignee_ids[]=0&assignee_ids[]=12 returns issues that are unassigned or assigned to user 12.
  • An exclusion keeps issues without a value. exclude_assignee_ids[]=12 still returns unassigned issues. Add 0 to the exclude list to drop them too: exclude_epic_ids[]=0 returns 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 ​

ParameterTypeRequiredDescription
group_bystringYeslane, assignee, sprint, epic, priority, type or label
Project Search filtersNoSee Project Search

Response Shape ​

The body is not wrapped in data.

FieldTypeDescription
totalintegerNumber of issues that match the filters
groupsobject[]One entry per value that has at least one issue, highest count first
groups[].keyintegerLane, 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[].labelstringLane title, user name, sprint title, epic title, label name, or the priority / type name
groups[].countintegerNumber 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 }
  ]
}

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 ​

ParameterTypeRequiredDescription
querystringNoText search across title, description, and key (max 200 chars)
project_idintegerNoFilter by project ID
lane_idintegerNoFilter by lane ID
assignee_idintegerNoFilter by assignee user ID
sprint_idintegerNoFilter by sprint ID
epic_idintegerNoFilter by epic ID
priorityintegerNoFilter by priority (0-4, see enums below)
typeintegerNoFilter by type (0-1, see enums below)
exclude_final_lanebooleanNoExclude issues in each project's highest-order lane (typically Done)
limitintegerNoMax 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.

FieldTypeDescription
dataIssueSearchResource[]Matching issues (never more than meta.limit)
meta.truncatedbooleantrue when the result set was capped at meta.limit; refine filters to see more matches
meta.countintegerNumber of issues actually returned in data
meta.limitintegerServer-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 ​

ParameterTypeRequiredDescription
cursorstringNoOpaque 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 ​

FieldTypeRequiredDescription
descriptionstringYesThe input text describing the desired issue
contextstringNoAdditional context prepended to the description

Response Fields ​

FieldTypeDescription
titlestringAI-generated issue title
descriptionstringAI-generated issue description (Markdown)
typeintegerSuggested issue type (0 Feature, 1 Bug)
priorityintegerSuggested priority (0-4)
intentstringPipeline result intent: create, update, or respond
reasoningstring|nullExplanation when intent is update or respond
issue_idinteger|nullExisting issue ID when intent is update
pipelinearray|nullPipeline step summary (see below), null when empty

Pipeline Step Object ​

Each entry in the pipeline array describes the result of one agent step:

FieldTypeDescription
stepstringAgent name: validate, duplicate_check, research, classify, write
statusinteger0 Success, 1 Warning, 2 Skipped
messagestringHuman-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:

FieldTypeDescription
stepstringAgent name (same as pipeline step names)
phaseinteger0 Pending, 1 Started, 2 Completed
messagestringHuman-readable progress message (English)
stepIndexintegerZero-based step position (0-4)
totalStepsintegerTotal 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 ​

ValueLabel
0Highest
1High
2Medium
3Low
4Lowest

Type ​

ValueLabel
0Feature
1Bug

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 ​