Docs
Search docs

List a pull request's activity timeline

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

listPullRequestTimeline

The pull request's activity, oldest first: opened, each push, each review, the merge, and the recorded transitions — closed, reopened (mirrors), draft and ready-for-review, title and base changes, auto-merge enabled and disabled (with the reason when it was not the user's own). Comments are listed by the comments endpoint; interleave the two by timestamp. Transitions older than their recording are absent; opened, pushes, reviews and the merge are present for every pull request.

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/NUMBER/timeline'

fetch

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

Path parameters

ownerrequired

Repository owner or organization slug.

string

reporequired

Repository name.

string

numberrequired

Change request number.

integerint32

Responses

200Timeline

application/json

PrTimelineResponse

object

A pull request's activity, oldest first. Comments are not included: they are listed by the comments endpoint, and a client interleaves the two by timestamp. History starts where recording started: opened, pushes and reviews are known for every pull request; the merge appears for every pull request merged since merges were recorded (one merged before that has no merge time, so no merged item); closes, draft changes, title and base changes and auto-merge on/off appear only from when they began to be recorded. An object rather than a bare list, so a cursor can be added.

itemsrequired

array

items

PrTimelineItem

object

One timeline item.

actor_display_name

string | null

actor_github_login

string | null

actor_handle

string | null

Actor projection, filled the same ghost-safe way as a comment's author fields.

actor_id

string | null

Account that did it. `null` when no account is known or shown: GitHub for a mirrored pull request, the merge queue acting on its own, the first revision (the `opened` item names the author), a push recorded before pushers were, any push to a pull request from a fork (the pusher is a fork's account, not disclosed on the target), or an imported merge (GitHub does not name the merger).

actor_kind
one of
atrequired

integerint64

Milliseconds since the epoch.

eventrequired
PrTimelineEvent
idrequired

string

Stable within the pull request: `opened`, `rev:{n}`, `review:{id}`, `event:{id}` or `merged`.

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.

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"

PrTimelineEvent

What happened, discriminated by `kind`.

one of
  • object

    kindrequired

    string

    "opened"

  • object

    A new head: the first revision, or a later push / force-push.

    base_commit

    string | null

    The diff base captured for this revision, when one was.

    head_commitrequired

    string

    40-char Git OID hex.

    kindrequired

    string

    "pushed"

    previous_base_commit

    string | null

    The previous revision's captured base. A base that differs from it means the push rebased onto a new target.

    previous_head_commit

    string | null

    The previous revision's head; absent on the first revision.

    revision_numberrequired

    integerint32

  • object

    decisionrequired

    ReviewDecision

    string

    Review decision on a pull request.

    "approved""changes_requested""commented"

    kindrequired

    string

    "reviewed"

    review_idrequired

    string

    summaryrequired

    string

  • object

    kindrequired

    string

    "merged"

    merge_commit

    string | null

    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

    merged_viarequired

    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"

  • object

    kindrequired

    string

    "closed"

  • object

    kindrequired

    string

    "reopened"

  • object

    kindrequired

    string

    "converted_to_draft"

  • object

    kindrequired

    string

    "ready_for_review"

  • object

    fromrequired

    string

    kindrequired

    string

    "title_changed"

    torequired

    string

  • object

    The target branch moved: a stack collapse retargeted this pull request.

    fromrequired

    string

    kindrequired

    string

    "base_changed"

    torequired

    string

  • object

    kindrequired

    string

    "auto_merge_enabled"

  • object

    Auto-merge was turned off; `reason` is set when it was not the user's own request (a push, or a merge-queue refusal).

    kindrequired

    string

    "auto_merge_disabled"

    reason

    string | null

  • object

    A merge requested by hand left the merge queue without merging; `reason` is the refusal the queue recorded (open vocabulary). An auto-merge's ejection is its `auto_merge_disabled` instead.

    kindrequired

    string

    "merge_queue_ejected"

    reason

    string | null