Docs
Search docs

The caller's open review requests or open pull requests, across every workspace

GET/api/user/pulls

getUserPulls

Account-scoped (every workspace). `filter` is `review_requested` (default; newest request first) or `authored` (most recently updated first). At most 100 rows; `truncated` says more exist. Every row is one the caller can read — the same decision the PR page makes, asked per repo — so a revoked grant or a repo gone private drops its rows.

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/user/pulls'

fetch

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

Query parameters

filter

`review_requested` (open PRs the caller is a requested reviewer on) or `authored` (the caller's own open PRs). **Omitted means `review_requested`**, the page's first section.

PullInvolvement

string

How the viewer is involved in an open pull request — the axis the account-scoped `GET /api/user/pulls` lists on (the `/reviews` page's two sections). `ReviewRequested`: the viewer is a requested reviewer; `Authored`: the viewer opened it.

"review_requested""authored"

Responses

200The caller's pull requests

application/json

UserPullsResponse

object

`GET /api/user/pulls`: the caller's open pull requests on one axis.

itemsrequired

array

At most 100 rows: newest request first for `review_requested`, most recently updated first for `authored`.

items

WorkPullRequest

object

One pull request in the viewer's work, with enough beside it to render a `PrListRow` and route to the PR.

checks
ownerrequired

string

The target repo's owner slug — the `{owner}` of its route.

reporequired

string

The target repo's slug — the `{repo}` of its route.

truncatedrequired

boolean

`true` when more rows exist than were returned — a fact from an over-fetch, never a guess from a full list.

403Forbidden — one of: forbidden, needs_scope

application/json

one of
  • ErrorForbidden
  • NeedsScopeError

    object

    `403` body returned when a classic OAuth token is asked for something its granted scopes do not cover — an explicit private listing without `repo`, or the one-click GitHub mirror without `admin:public_key`. `scope` names the missing grant so the client can say exactly what the token lacks. There is no elevation route to widen it any more (a re-link goes through the GitHub App, whose tokens carry no scopes), so the client's answer is to say so, never to start a handshake.

    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
  • 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.

PullRequestChecksRollup

object

One PR's CI roll-up, keyed by its repo-scoped `number`. Deliberately NOT a field on [`PullRequestResponse`]. A PR's title, branches and author change when somebody edits them; its check state changes every few seconds while a run is in flight. Folding the second into the first would make every CI heartbeat invalidate the whole list payload — the client would refetch titles and branches to learn that a dot went green, and a prerendered snapshot would bake in a verdict that expires before anyone reads it. Two payloads, two cache postures.

checks_summaryrequired

ChecksSummary

object

Roll-up of the check states for the merge box. Counted over the newest-run-per-context set (`neosource_forge::head_checks`), so a run that a re-run superseded is not a second check.

failedrequired

integerint32

`failure` + `error` states — both render red and both block.

pendingrequired

integerint32

skipped

integerint32

Checks whose job never ran (gated off by a job-level `if:`, or a `needs:` predecessor did not succeed). Counted separately from `success`; `success + failed + pending + skipped == total`.

successrequired

integerint32

Checks that ran and passed. Deliberately EXCLUDES skipped ones: a skipped check carries `state: success` so it cannot block a merge, but counting it as "passed" would tell the reader that work happened when it did not — the same claim the per-check `skipped` flag exists to stop the UI making.

superseded

integerint32

Checks the merge queue superseded (`CheckRun::superseded`, ADR 0160): a SUBSET of `pending` (`superseded <= pending`) — they are counted in `pending` too, so a reader that ignores this field still sees the head as not green — never in `failed` (nothing failed). Render `pending - superseded` as in flight and these as superseded.

totalrequired

integerint32

head_commit

string | null

The head the roll-up was computed against (40-char Git OID hex), or `null` before the first push resolves one. Present so a client can tell "checked, nothing ran" from "there is nothing to check yet".

numberrequired

integerint32

Repo-scoped PR number — the join key back to the list payload.

queue_candidate

string | null

The merge-queue candidate (40-char Git OID hex) `checks_summary` was computed against: the PR's live queue entry is speculative, frozen on the PR's current head, and its candidate has reported at least one check. `null` when the roll-up is the head's.

workflows

array

The roll-up split by workflow, over the same commit and the same collapse as `checks_summary`: each workflow's newest surviving run, in row order (by path, then id). What a PR row paints one icon per.

items

WorkflowCheck

object

One workflow's newest run on a commit — one icon of a commit or PR row, beside the workflow's siblings, where a single roll-up would hide which workflow was red (`WorkflowVerdict` in neosource-core). Rows receive these already ordered: by `workflow_path`, then `workflow_id`, an unregistered workflow last — never by state.

run_idrequired

string

The newest run (latest attempt) of this workflow on the commit (UUID).

run_numberrequired

integerint64

skippedrequired

boolean

Every job of the run was skipped: `status` is `succeeded`, but no work ran.

statusrequired

WorkflowRunStatus

string

Lifecycle of a workflow run. Mirrors the GHA-shaped surface that the SPA renders. The serde `snake_case` strings are the wire vocabulary the API emits (pinned by the `enum_wire_consistency` tests).

"pending_approval""queued""quota_blocked""concurrency_blocked""running""cancelling""succeeded""failed""cancelled"

supersededrequired

boolean

The merge queue superseded and cancelled this run (ADR 0160): render neutral, neither red nor in flight.

workflow_idrequired

string

Workflow id (UUID).

workflow_namerequired

string

The workflow's declared `name:` as the run pinned it.

workflow_path

string | null

The workflow file's repo path; `null` when the workflow is no longer registered.

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