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
Responses
200Link list
application/json
403Forbidden — one of: forbidden, needs_scope
application/json
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
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
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_idrequirednumberrequiredprimary_repo_id`None` for a team-scoped ticket — this is the field the client branches on to choose between `KEY-N` and `owner/repo#N`.
repo_namerepo_ownerRepo-bound targets only: the repo route for `owner/repo#N`.
statusrequiredteam_keyTicket targets only: `KEY` in `KEY-N`.
team_ownerTicket 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