neosourceDocs
Search docs

List outgoing links for an issue (covers both directions)

GET/api/issues/{issue_id}/links

listIssueLinks

Every link pair materialises a row keyed on the source side, so this endpoint returns blocks / blocked_by / related / duplicate_of / duplicates rows in one shot.

Authentication is not described for this operation in the spec — that does not mean it is public. Check tokens and scopes.

curl

curl -X GET 'https://neosource.dev/api/issues/ISSUE_ID/links'

fetch

fetch("https://neosource.dev/api/issues/ISSUE_ID/links", {
  method: "GET",
});

Path parameters

issue_idrequired

string

Responses

200Link list

application/json

IssueLinkListResponse

object

linksrequired

array

items

IssueLinkResponse

object

created_atrequired

integerint64

created_byrequired

string

link_kindrequired
IssueLinkKind
source_issue_idrequired

string

target
target_issue_idrequired

string

403Forbidden — one of: forbidden, needs_scope

application/json

one of
  • ErrorForbidden
  • NeedsScopeError

    object

    `403` body returned when listing private repos but the linked identity lacks the required provider scope. The SPA turns this into an incremental-authorization prompt (Tier 2) that calls the `/elevate` OAuth endpoint with this `scope`.

    errorrequired

    string

    Always `needs_scope` — this body exists to carry the extra fields that kind needs.

    "needs_scope"

    scoperequired

    string

    The provider scope to request via elevation (e.g. `"repo"`).

Standard errors

Bodies documented once for the whole API — see standard errors.

  • 400Bad Request — one of: invalid_input
  • 404Not Found — one of: not_found
  • 429Rate limited — retry after the `Retry-After` header
  • 500Internal server error
  • 503Service temporarily unavailable / at capacity — retry after the `Retry-After` header
  • 504Gateway timeout — the request exceeded the server's handling budget

Schemas

Referenced above. Listed here rather than expanded inline, so the same definition is not repeated at every level.

IssueLinkKind

string

How one issue relates to another. Bidirectional pairs: - `Blocks` ↔ `BlockedBy` - `Related` ↔ `Related` (symmetric) - `DuplicateOf` ↔ `Duplicates` - `Tracks` ↔ `TrackedBy` `Tracks` is the one kind with a DIRECTION the product constrains beyond what this enum can express: a team ticket tracks a repo issue, never the reverse, and `TrackedBy` is only ever the inverse row the pairing writes. `IssueService::create_link` enforces both (ADR 0071 § Amendment 2026-09-05 §3) — the type permits the shape, the service refuses it.

"blocks""blocked_by""related""duplicate_of""duplicates""tracks""tracked_by"

IssueLinkTargetResponse

object

The link TARGET, resolved to something renderable. A link row carries two UUIDs, so the rail could only ever render `target_issue_id.slice(0, 8)` — a hex prefix is not a reference. These are the fields needed to write `ENG-9` or `acme/api#1526` and route to it. Filled by the route (only it holds the stores) and **best-effort**, the same posture as `IssuePrLinkResponse`: a lookup miss leaves the resolved fields `None` and the client falls back to the bare id, rather than the whole read failing because one repo row moved. Which resolved fields are present follows the target's own kind: `team_key`/`team_owner` for a ticket (`primary_repo_id` null), `repo_owner`/`repo_name` for a repo-bound issue. Minimal disclosure — the client branches on `primary_repo_id` anyway, so the other pair would never be rendered.

issue_idrequired

string

numberrequired

integerint32

primary_repo_id

string | null

`None` for a team-scoped ticket — this is the field the client branches on to choose between `KEY-N` and `owner/repo#N`.

repo_name

string | null

repo_owner

string | null

Repo-bound targets only: the repo route for `owner/repo#N`.

statusrequired

IssueStatus

string

"backlog""todo""in_progress""in_review""done""cancelled""duplicate"

team_key

string | null

Ticket targets only: `KEY` in `KEY-N`.

team_owner

string | null

Ticket targets only: the workspace slug the ticket's route needs (`/{owner}/teams/{key}/issues/{n}`). Without it the ref can only be inert text.

titlerequired

string