Docs
Search docs

List pull requests

GET/api/repos/{owner}/{repo}/pulls

listPullRequests

Lists pull requests for a repository, newest number first, with an optional status filter. Keyset-paginated: pass a previous response's `next_cursor` back as `cursor` for the next (older) page. `counts` (the whole-repo tab badges) is present only on a request with no `cursor`. `offset` is deprecated but still honoured; `cursor` together with a positive `offset` is a 400.

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/repos/OWNER/REPO/pulls'

fetch

fetch("https://neosource.dev/api/repos/OWNER/REPO/pulls", {
  method: "GET",
});

neo

neo pr list OWNER/REPO

Path parameters

ownerrequired

Repository owner or organization slug.

string

reporequired

Repository name.

string

Query parameters

status

Filter by status.

PullRequestStatus

string

Lifecycle status of a pull request.

"open""merged""closed"

limit

Maximum number of results.

integerint64

default30

offset

Deprecated: pass `cursor` instead. Rows to skip before the page; still honoured, and a request carrying it still gets `counts`. Sending a positive `offset` together with `cursor` is a 400.

integerint64

default0

cursor

Opaque pagination cursor from a previous response's `next_cursor`: the next (older) page. A request with a cursor gets `counts: null`.

string

view

Row shape: `full` (default) or `summary`. Any other value is a 400.

string

Which row shape `GET …/pulls` answers with. `full` (the default) is every field of [`PullRequestResponse`], byte for byte what the route has always sent — the shape every `neo` build decodes, and prints through `neo pr list --json`. `summary` is [`PullRequestListItem`]: what a list renders, without the Markdown body or the detail-only fields. Only a caller that asks gets it.

"full""summary"

Responses

200Change request list

application/json

PullRequestListResponse

object

counts
one of
  • PullRequestStatusCounts

    object

    Whole-repo pull request counts by status, plus the merge queue's active membership — the numbers a PR list's tab strip renders. Whole repo, NOT the page: a tab badge says how many PRs are in that state, and a page of 30 cannot answer that. `queued` counts active `merge_queue_entries` in `queued` / `testing` / `merging` — every row the merge-queue view lists — and is a subset of `open` (a queued PR is still an open one), so the four do not sum to anything.

    closedrequired

    integerint64

    mergedrequired

    integerint64

    openrequired

    integerint64

    queuedrequired

    integerint64

  • null

next_cursor

string | null

Pass back as `cursor` for the next (older) page; `null` on the last page.

pull_requestsrequired

array

Every row in the one shape the request's `view` names.

items

PullRequestListRow

One row of `GET …/pulls`, in the shape [`PrListView`] asked for. Untagged: a `full` row is exactly a [`PullRequestResponse`] on the wire, so a decoder that knows only that type reads the default response unchanged.

Standard errors

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

  • 400Bad Request — one of: invalid_input
  • 403Forbidden — one of: forbidden
  • 404Not Found — one of: not_found
  • 423Locked — one of: busy
  • 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.

PullRequestResponse

object

author_display_name

string | null

author_github_login

string | null

Last-seen GitHub login for authors with a linked GitHub identity (always set for ghosts).

author_handle

string | null

Author fields resolved at read time (route-layer enrichment via the ghost-safe authors projection — plans/github-metadata-import.md §7), exactly as on [`crate::IssueResponse`]. Filled on every READ — list, detail, and both context packs. `None` on a mutation receipt (the caller already has the row it just wrote) or when the account row is gone.

author_idrequired

string

author_kind
one of
  • AccountKind

    string

    Rust mirror of the Pg `account_kind` enum (`accounts.kind`). `Ghost` rows are imported-author placeholders (GitHub metadata import, plans/github-metadata-import.md §6): `handle = gh-<github-id>`, `personal_workspace_id` NULL, no password, no sessions. They must never flow through `get_account*` (`AccountRow::into_record` CorruptData-errors on the NULL workspace) — author rendering goes through `PgGithubImportStore::authors_projection`, and claim-by-proof later promotes or merges them.

    "human""bot""service""runner""ghost"

  • null

auto_merge_enabled_at

integer | nullint64

When auto-merge was armed (ms since epoch); set together with `auto_merge_enabled_by`.

auto_merge_enabled_by

string | null

Account id that armed auto-merge (merge-when-green); `null` = off. On an OPEN PR this means "will merge, as this account, the moment the merge gate reports mergeable". Cleared automatically when the head advances (new commits disarm).

change_idrequired

string

32-byte durable change identity (hex). Survives force-push / rebase.

created_atrequired

integerint64

descriptionrequired

string

draftrequired

boolean

head_commit

string | null

40-char Git OID hex of the source branch's current tip, or `null` before the first push resolves one.

merge_commit

string | null

40-char Git OID hex of the commit the merge minted, or `null` when it minted none: an import, or a merge whose source was already reachable from the target.

merge_method
one of
  • MergeMethod

    string

    How a pull request's commits land on its target branch. A repo carries two settings over this enum (`migrations/20260820120000_repo_merge_methods.sql`): the set it *permits* (`RepoRecord::allowed_merge_methods`, never empty) and the one the merge path reaches for when the request names none (`RepoRecord::default_merge_method`, always a member of that set — enforced by the settings route, which is the only place both columns are visible at once). `Default` is [`MergeMethod::Merge`] to match the column default, which is itself the pre-existing behaviour of every repo: before this setting existed, a merge always minted a two-parent commit. `Rebase` is deliberately absent rather than merely unimplemented — it replays N commits, can conflict per commit, and rewrites the SHAs the stacked-PR re-parent invariant leans on (`plans/archive/pr-merge-methods-2026-08.md` §"Not doing"). Adding it later is one `ALTER TYPE merge_method ADD VALUE` plus an arm here, which is why the stored shape is an enum array and not a pair of booleans.

    "merge""squash"

  • null

merge_queue
one of
  • PrMergeQueueSummary

    object

    `PartialEq` because the drive publishes an entry exactly when this — the face the API shows — differs from what the entry showed before the step (`merge_queue.rs`, `QueueFrames`): a record rewrite that changes nothing visible (a `blocked_by` refresh every tick, a `waits` verdict) must not nudge every viewer's queue.

    blocked_byrequired

    array

    items

    string

    enqueued_atrequired

    integerint64

    entry_idrequired

    string

    head_commitrequired

    string

    methodrequired

    MergeMethod

    string

    How a pull request's commits land on its target branch. A repo carries two settings over this enum (`migrations/20260820120000_repo_merge_methods.sql`): the set it *permits* (`RepoRecord::allowed_merge_methods`, never empty) and the one the merge path reaches for when the request names none (`RepoRecord::default_merge_method`, always a member of that set — enforced by the settings route, which is the only place both columns are visible at once). `Default` is [`MergeMethod::Merge`] to match the column default, which is itself the pre-existing behaviour of every repo: before this setting existed, a merge always minted a two-parent commit. `Rebase` is deliberately absent rather than merely unimplemented — it replays N commits, can conflict per commit, and rewrites the SHAs the stacked-PR re-parent invariant leans on (`plans/archive/pr-merge-methods-2026-08.md` §"Not doing"). Adding it later is one `ALTER TYPE merge_method ADD VALUE` plus an arm here, which is why the stored shape is an enum array and not a pair of booleans.

    "merge""squash"

    position

    integer | nullint64

    speculative_head

    string | null

    staterequired

    MergeQueueState

    string

    "queued""testing""merging""ejected"

    target_branchrequired

    string

  • null

merged_at

integer | nullint64

When the merge landed (ms since epoch). Unlike `updated_at` this never moves again, so it is the field to render as "merged <when>".

merged_by

string | null

Account credited with the merge, `null` until the PR merges — and also on a merged GitHub mirror, whose upstream merger the import payload does not carry. For a merge made with a service token this is the token's OWNER; the token itself is recorded in the operation log, not here.

merged_by_display_name

string | null

merged_by_github_login

string | null

merged_by_handle

string | null

Merger fields resolved at read time, exactly as the `author_*` group above and through the same batched projection call. `None` on a mutation receipt, on an unmerged PR, and when the account row is gone.

merged_by_kind
one of
  • AccountKind

    string

    Rust mirror of the Pg `account_kind` enum (`accounts.kind`). `Ghost` rows are imported-author placeholders (GitHub metadata import, plans/github-metadata-import.md §6): `handle = gh-<github-id>`, `personal_workspace_id` NULL, no password, no sessions. They must never flow through `get_account*` (`AccountRow::into_record` CorruptData-errors on the NULL workspace) — author rendering goes through `PgGithubImportStore::authors_projection`, and claim-by-proof later promotes or merges them.

    "human""bot""service""runner""ghost"

  • null

merged_via
one of
  • MergedVia

    string

    How a pull request's merge was triggered — the companion to [`MergeMethod`], which says what the merge *minted*. The two are independent: a squash can land either by hand or by an armed auto-merge, and an imported mirror carries a trigger with no method at all.

    "manual""auto""import"

  • null

mirror_head_repo

string | null

For a fork mirror, the GitHub `owner/name` of the fork the head lives in — a non-null value means "no local branch" (fork diff renders off the pinned OID). `null` for native PRs and same-repo mirrors.

numberrequired

integerint32

originrequired

PrOrigin

string

Provenance of a pull request. `Native` PRs are created in neosource and are fully mutable; `GithubMirror` PRs are read-only imports of GitHub pull requests (one-directional metadata sync). Store-level single-row lookups filter to `Native` so a fork mirror — which stores GitHub head-ref names like `main`/`patch-1` — can never mask a native PR nor be advanced by a native push.

"native""github_mirror"

parent_pr_id

string | null

Parent PR id when this PR is part of a stack (its `target_branch` matches another open PR's `source_branch`). `None` for a non-stacked PR or for the bottom of a stack.

pr_idrequired

string

repo_idrequired

string

source_branchrequired

string

source_repo_id

string | null

Repo the source branch lives in. Equals `repo_id` for a same-repo PR; the contributor's fork for a fork PR. `null` once the source repository was deleted — such a pull request is kept as history but can never merge.

statusrequired

PullRequestStatus

string

Lifecycle status of a pull request.

"open""merged""closed"

target_branchrequired

string

titlerequired

string

updated_atrequired

integerint64

PullRequestListItem

object

One row of `GET …/pulls?view=summary`: what a list renders, not the whole PR. The fields a list surface reads: - the row itself (`PrListRow.svelte`, `neo pr list`'s table): title, number, branches, author, state badge, queue badge, mirror marker, and `head_commit` — the join key the `pulls/checks` roll-up is gated on; - the PR page's header, which paints from the clicked row before the detail lands (`pullDetailPlaceholder` in `neosource-app/src/lib/queries/pulls.ts`): the `merged_*` group feeds its "merged by X" line, and `pr_id` + `auto_merge_enabled_by` decide whether the row may seed it at all; - `author_id`: the author label's fallback when no handle resolved (`authorLabel` in `neosource-app/src/lib/utils/authors.ts`). Not carried, because no list reads them: the Markdown `description` (unbounded), `repo_id` (the request's own repo), `change_id`, `updated_at`, and the detail-only `source_repo_id`, `parent_pr_id`, `auto_merge_enabled_at`, `merge_commit`. The default `full` view and the detail endpoint carry them.

author_display_name

string | null

author_github_login

string | null

author_handle

string | null

As on [`PullRequestResponse`]: resolved at read time, `None` when the account row is gone.

author_idrequired

string

author_kind
one of
  • AccountKind

    string

    Rust mirror of the Pg `account_kind` enum (`accounts.kind`). `Ghost` rows are imported-author placeholders (GitHub metadata import, plans/github-metadata-import.md §6): `handle = gh-<github-id>`, `personal_workspace_id` NULL, no password, no sessions. They must never flow through `get_account*` (`AccountRow::into_record` CorruptData-errors on the NULL workspace) — author rendering goes through `PgGithubImportStore::authors_projection`, and claim-by-proof later promotes or merges them.

    "human""bot""service""runner""ghost"

  • null

auto_merge_enabled_by

string | null

Account id that armed auto-merge; `null` = off.

created_atrequired

integerint64

draftrequired

boolean

head_commit

string | null

40-char Git OID hex of the source branch's current tip.

merge_method
one of
  • MergeMethod

    string

    How a pull request's commits land on its target branch. A repo carries two settings over this enum (`migrations/20260820120000_repo_merge_methods.sql`): the set it *permits* (`RepoRecord::allowed_merge_methods`, never empty) and the one the merge path reaches for when the request names none (`RepoRecord::default_merge_method`, always a member of that set — enforced by the settings route, which is the only place both columns are visible at once). `Default` is [`MergeMethod::Merge`] to match the column default, which is itself the pre-existing behaviour of every repo: before this setting existed, a merge always minted a two-parent commit. `Rebase` is deliberately absent rather than merely unimplemented — it replays N commits, can conflict per commit, and rewrites the SHAs the stacked-PR re-parent invariant leans on (`plans/archive/pr-merge-methods-2026-08.md` §"Not doing"). Adding it later is one `ALTER TYPE merge_method ADD VALUE` plus an arm here, which is why the stored shape is an enum array and not a pair of booleans.

    "merge""squash"

  • null

merge_queue
one of
  • PrMergeQueueSummary

    object

    `PartialEq` because the drive publishes an entry exactly when this — the face the API shows — differs from what the entry showed before the step (`merge_queue.rs`, `QueueFrames`): a record rewrite that changes nothing visible (a `blocked_by` refresh every tick, a `waits` verdict) must not nudge every viewer's queue.

    blocked_byrequired

    array

    items

    string

    enqueued_atrequired

    integerint64

    entry_idrequired

    string

    head_commitrequired

    string

    methodrequired

    MergeMethod

    string

    How a pull request's commits land on its target branch. A repo carries two settings over this enum (`migrations/20260820120000_repo_merge_methods.sql`): the set it *permits* (`RepoRecord::allowed_merge_methods`, never empty) and the one the merge path reaches for when the request names none (`RepoRecord::default_merge_method`, always a member of that set — enforced by the settings route, which is the only place both columns are visible at once). `Default` is [`MergeMethod::Merge`] to match the column default, which is itself the pre-existing behaviour of every repo: before this setting existed, a merge always minted a two-parent commit. `Rebase` is deliberately absent rather than merely unimplemented — it replays N commits, can conflict per commit, and rewrites the SHAs the stacked-PR re-parent invariant leans on (`plans/archive/pr-merge-methods-2026-08.md` §"Not doing"). Adding it later is one `ALTER TYPE merge_method ADD VALUE` plus an arm here, which is why the stored shape is an enum array and not a pair of booleans.

    "merge""squash"

    position

    integer | nullint64

    speculative_head

    string | null

    staterequired

    MergeQueueState

    string

    "queued""testing""merging""ejected"

    target_branchrequired

    string

  • null

merged_at

integer | nullint64

merged_by

string | null

merged_by_display_name

string | null

merged_by_github_login

string | null

merged_by_handle

string | null

merged_by_kind
one of
  • AccountKind

    string

    Rust mirror of the Pg `account_kind` enum (`accounts.kind`). `Ghost` rows are imported-author placeholders (GitHub metadata import, plans/github-metadata-import.md §6): `handle = gh-<github-id>`, `personal_workspace_id` NULL, no password, no sessions. They must never flow through `get_account*` (`AccountRow::into_record` CorruptData-errors on the NULL workspace) — author rendering goes through `PgGithubImportStore::authors_projection`, and claim-by-proof later promotes or merges them.

    "human""bot""service""runner""ghost"

  • null

merged_via
one of
  • MergedVia

    string

    How a pull request's merge was triggered — the companion to [`MergeMethod`], which says what the merge *minted*. The two are independent: a squash can land either by hand or by an armed auto-merge, and an imported mirror carries a trigger with no method at all.

    "manual""auto""import"

  • null

mirror_head_repo

string | null

Fork mirror's GitHub `owner/name`; see [`PullRequestResponse`].

numberrequired

integerint32

originrequired

PrOrigin

string

Provenance of a pull request. `Native` PRs are created in neosource and are fully mutable; `GithubMirror` PRs are read-only imports of GitHub pull requests (one-directional metadata sync). Store-level single-row lookups filter to `Native` so a fork mirror — which stores GitHub head-ref names like `main`/`patch-1` — can never mask a native PR nor be advanced by a native push.

"native""github_mirror"

pr_idrequired

string

source_branchrequired

string

statusrequired

PullRequestStatus

string

Lifecycle status of a pull request.

"open""merged""closed"

target_branchrequired

string

titlerequired

string