Merge requests
Reference for the merge-request endpoints under
/api/v1/repos/{owner}/{repo}/merges: opening, reading, reviewing, updating,
and merging merge requests, plus their commits, diffs, review comments, and
review decisions.
Resource overview
A merge request proposes integrating a source branch into a target branch in one repository. Every endpoint on this page is scoped to a single repository and hangs off the base path:
/api/v1/repos/{owner}/{repo}/merges
A merge request is addressed by its per-repository number (1-based, assigned
at creation), not by its id UUID. The sub-resources documented here are the
merge request itself, its commits, its diff (whole and per file), its review
comments, and its review decisions.
/merges is the canonical path. The older /pulls paths are deprecated: each
answers with an HTTP 308 Permanent Redirect to the equivalent /merges path,
preserving the request method. Call /merges directly. The naming rule and the
wire-level identifiers that intentionally keep the pull name (the
pull_request webhook event and the /ws/repos/{owner}/{repo}/pulls/{number}
live-update WebSocket) are described in
conventions.md.
Auth & scopes
See conventions.md for the accepted credential types and how scopes are enforced. Merge requests are a repository sub-resource, so OAuth2 and personal-access-token callers use the repository scopes:
| Operation | OAuth2 scope | PAT scope |
|---|---|---|
| Read (list, get, commits, files, comments) | read:repo |
repo:read |
| Write (create, update, merge, comment, review) | write:repo |
repo:write |
The operative authorization is a per-operation repository permission, checked in
addition to the scope above and listed with each endpoint: a read permission for
the read endpoints, an open permission for create, an update/close permission
for PATCH, and a merge permission for the merge action. Comment edits and
deletes are restricted to the comment's author or a repository administrator.
Reads of a private repository's merge requests follow the
enumeration-resistant 404 rule: a caller who may not read the repository
receives 404 Not Found, not 403. See errors.md.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET |
/api/v1/repos/{owner}/{repo}/merges |
List merge requests (paginated) |
GET |
/api/v1/repos/{owner}/{repo}/merges/{number} |
Fetch one merge request |
POST |
/api/v1/repos/{owner}/{repo}/merges |
Open a merge request |
PATCH |
/api/v1/repos/{owner}/{repo}/merges/{number} |
Update state, draft flag, or description |
POST |
/api/v1/repos/{owner}/{repo}/merges/{number}/merge |
Merge a merge request |
GET |
/api/v1/repos/{owner}/{repo}/merges/{number}/commits |
List the commits in a merge request |
GET |
/api/v1/repos/{owner}/{repo}/merges/{number}/files |
List changed files (paginated summary) |
GET |
/api/v1/repos/{owner}/{repo}/merges/{number}/files/{path} |
Fetch the structured diff for one file |
GET |
/api/v1/repos/{owner}/{repo}/merges/{number}/comments |
List review comments |
POST |
/api/v1/repos/{owner}/{repo}/merges/{number}/comments |
Create a review comment |
PATCH |
/api/v1/repos/{owner}/{repo}/merges/{number}/comments/{cid} |
Edit or resolve a review comment |
DELETE |
/api/v1/repos/{owner}/{repo}/merges/{number}/comments/{cid} |
Delete a review comment |
POST |
/api/v1/repos/{owner}/{repo}/merges/{number}/reviews |
Submit a review decision |
POST |
/api/v1/repos/{owner}/{repo}/merges/prepare-url |
Build a prefilled new-merge-request URL |
The merge request object
List, get, create, update, and merge all return the same merge request object. List rows are lightweight: they carry the core fields and the author, but the diff, line stats, commit count, reviewers, and labels are returned at their zero values. Fetch a single merge request to get the enriched object.
{
"id": "<uuid>",
"repo_id": "<uuid>",
"number": 42,
"title": "Add retry budget to the runner poller",
"description": "Markdown body of the merge request.",
"state": "open",
"is_draft": false,
"source_branch": "feature/retry-budget",
"target_branch": "main",
"source_branch_protected": false,
"author": { "id": "<uuid>", "username": "alice", "display_name": "Alice", "avatar_url": "/user-content/..." },
"assignees": [],
"reviewers": [ { "user": { "id": "<uuid>", "username": "bob", "display_name": "Bob", "avatar_url": "..." }, "state": "approved" } ],
"labels": [ { "id": "<uuid>", "name": "backend", "color": "1f6feb" } ],
"milestone": null,
"ci_status": "none",
"commits_count": 3,
"additions": 48,
"deletions": 12,
"diff": "diff --git a/... b/...",
"diff_suppressed": false,
"files": [ ... ],
"diff_caps": { "max_file_bytes": 1048576, "max_total_bytes": 10485760, "max_file_lines": 5000, "max_files": 500 },
"merge_commit": "9f1c2e0...",
"created_at": "2026-06-01T12:00:00Z",
"updated_at": "2026-06-02T09:30:00Z",
"merged_at": "2026-06-02T09:30:00Z",
"closed_at": null
}
| Field | Type | Description |
|---|---|---|
id |
string | Stable UUID. Addressing uses number, not id. |
number |
integer | Per-repository merge-request number. |
state |
string | open, merged, or closed. |
is_draft |
boolean | Draft merge requests cannot be merged until cleared. |
source_branch / target_branch |
string | Branches being merged from / into. |
source_branch_protected |
boolean | Whether the source branch matches a protected-branch rule. |
author |
object | User summary: id, username, display_name, avatar_url. |
reviewers |
array | Each entry is { user, state }; state is pending, approved, or changes_requested. |
labels |
array | Each entry is { id, name, color }. |
ci_status |
string | Pipeline status for the merge request; none when no pipeline applies. |
commits_count |
integer | Commits reachable from the source branch but not the target. |
additions / deletions |
integer | Line counts across the diff. |
diff |
string | Full unified diff. Empty on list rows and whenever the diff is suppressed. |
diff_suppressed |
boolean | true when the diff body was deliberately omitted. |
diff_suppressed_reason |
string | Present only when diff_suppressed is true. One of git_backend_unavailable, repository_lookup_failed, compare_branches_failed, binary_files_only, diff_too_large, total_size_cap, file_count_cap. |
files |
array | Per-file projection of the diff. Omitted when empty; use the files endpoints for large diffs. |
diff_caps |
object | The operator-configured diff thresholds that drove suppression. Omitted when not applicable. |
merge_commit |
string | Merge commit SHA. Present once merged. |
created_at / updated_at |
string | RFC 3339 timestamps. |
merged_at / closed_at |
string | RFC 3339 timestamps; present once the merge request reaches that state. |
GET /api/v1/repos/{owner}/{repo}/merges
List a repository's merge requests, newest first, with pagination. Requires read access to the repository.
Path parameters
| Name | Type | Description |
|---|---|---|
owner |
string | Repository owner's username. |
repo |
string | Repository name. |
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
state |
string | open |
Filter by state: open, merged, closed, or all. |
page |
integer | 1 |
See Pagination. |
per_page |
integer | 25 |
Clamped to [1, 100]. See Pagination. |
Response
{
"items": [ { "id": "<uuid>", "number": 42, "title": "...", "state": "open", "...": "..." } ],
"total": 137,
"page": 1,
"per_page": 25
}
items are lightweight merge request objects (see above); fetch a single merge
request for the diff, stats, reviewers, and labels.
Status codes
| Status | When |
|---|---|
200 OK |
Listing succeeded. |
404 Not Found |
Repository missing, or private and not readable — see Errors. |
Example
curl -H "Authorization: Bearer <token>" \
"https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges?state=open&per_page=25"
GET /api/v1/repos/{owner}/{repo}/merges/{number}
Fetch one merge request, enriched with its diff, line stats, commit count, and reviewers. Requires read access to the repository.
Path parameters
| Name | Type | Description |
|---|---|---|
number |
integer | Per-repository merge-request number. |
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
algorithm |
string | operator default, else myers |
Line-diff algorithm: myers or histogram. |
Response
A single merge request object (see The merge request object).
Status codes
| Status | When |
|---|---|
200 OK |
Merge request returned. |
400 Bad Request |
number is not an integer, or algorithm is unknown. |
404 Not Found |
Repository or merge request missing, or private and not readable. |
Example
curl -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/{number}
POST /api/v1/repos/{owner}/{repo}/merges
Open a merge request. Requires permission to open merge requests in the repository.
Request body
{
"title": "Add retry budget to the runner poller",
"description": "Markdown body of the merge request.",
"source_branch": "feature/retry-budget",
"target_branch": "main",
"draft": false
}
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | yes | Merge-request title. |
source_branch |
string | yes | Branch to merge from. |
target_branch |
string | yes | Branch to merge into. |
description |
string | no | Markdown body. body is accepted as an alias; when both are sent, description wins. |
draft |
boolean | no | Open as a draft. Defaults to false. |
Response
201 Created with the new merge request object.
Status codes
| Status | When |
|---|---|
201 Created |
Merge request opened. |
400 Bad Request |
Invalid body, missing title, or missing source_branch/target_branch. |
403 Forbidden |
The caller may not open merge requests here. A non-member of a private repository receives 403, not 404: opening gates on a per-operation permission, so this endpoint does not follow the enumeration-resistant 404 rule. |
404 Not Found |
Repository missing. |
409 Conflict |
A required-pipeline rule blocked the open; the body carries the rule's reason code. |
Example
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"title":"Add retry budget","source_branch":"feature/retry-budget","target_branch":"main"}' \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges
PATCH /api/v1/repos/{owner}/{repo}/merges/{number}
Change a merge request's state, draft flag, or description. Send only the fields you want to change; sending none is an error. Requires update permission (closing requires the close permission).
Path parameters
| Name | Type | Description |
|---|---|---|
number |
integer | Per-repository merge-request number. |
Request body
{
"state": "closed",
"draft": false,
"description": "Updated Markdown body."
}
| Field | Type | Description |
|---|---|---|
state |
string | open or closed. Other values are rejected. |
draft |
boolean | true sets the draft flag; false clears it. Only valid while the merge request is open. |
description |
string | New Markdown body. body is accepted as an alias. |
Response
200 OK with the updated merge request object.
Status codes
| Status | When |
|---|---|
200 OK |
Update applied. |
400 Bad Request |
No fields supplied, an invalid state, or a draft change on a closed/merged merge request. |
403 Forbidden |
The caller may not update or close this merge request. A non-member of a private repository receives 403, not 404: updating gates on a per-operation permission, so this endpoint does not follow the enumeration-resistant 404 rule. |
404 Not Found |
Repository or merge request missing. |
Example
curl -X PATCH -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"state":"closed"}' \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/{number}
POST /api/v1/repos/{owner}/{repo}/merges/{number}/merge
Merge a merge request into its target branch. Requires merge permission. A merge is refused while the merge request is a draft or has any non-approved review, and the synchronous merge may take longer than other API calls on large or contended repositories.
Path parameters
| Name | Type | Description |
|---|---|---|
number |
integer | Per-repository merge-request number. |
Request body
{
"strategy": "merge",
"commit_message": "Merge feature/retry-budget into main",
"author_name": "Alice",
"author_email": "alice@example.com",
"delete_source_branch": true
}
| Field | Type | Description |
|---|---|---|
strategy |
string | merge (explicit merge commit), squash (squash source commits into one), or rebase (rebase source onto target). Defaults to merge when omitted. |
commit_message |
string | Message for the merge or squash commit. |
author_name / author_email |
string | Commit identity. Default to the caller's username when omitted. |
delete_source_branch |
boolean | Delete the source branch after a successful merge. Best-effort; protected branches and the default branch are never deleted, and a deletion failure does not roll back the merge. |
Response
200 OK with the merged merge request object (state is merged and
merge_commit is populated).
Status codes
| Status | When |
|---|---|
200 OK |
Merge completed. |
400 Bad Request |
Invalid body or an unknown strategy. |
403 Forbidden |
The caller may not merge this merge request. A non-member of a private repository receives 403, not 404: merging gates on a per-operation permission, so this endpoint does not follow the enumeration-resistant 404 rule. |
404 Not Found |
Repository or merge request missing. |
409 Conflict |
The merge request is a draft (draft_not_ready), has pending reviews (pending_reviews), conflicts with the target (merge conflict), or a required-pipeline rule blocked it (rule reason code). |
503 Service Unavailable |
The merge was canceled before completion; retry. |
504 Gateway Timeout |
The merge timed out; retry. |
Example
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"strategy":"squash","commit_message":"Add retry budget","delete_source_branch":true}' \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/{number}/merge
GET /api/v1/repos/{owner}/{repo}/merges/{number}/commits
List the commits a merge request contributes — reachable from the source branch but not the target — newest first. Requires read access to the repository.
Path parameters
| Name | Type | Description |
|---|---|---|
number |
integer | Per-repository merge-request number. |
Response
[
{ "sha": "9f1c2e0...", "message": "Add retry budget", "author": "Alice", "date": "2026-06-01T12:00:00Z" }
]
The response is always a JSON array, empty when the merge request contains no commits.
Status codes
| Status | When |
|---|---|
200 OK |
Commit list returned (possibly empty). |
400 Bad Request |
number is not an integer. |
404 Not Found |
Repository or merge request missing, or private and not readable. |
500 Internal Server Error |
The commits could not be enumerated (for example, a branch ref is gone on an unmerged merge request). |
Example
curl -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/{number}/commits
GET /api/v1/repos/{owner}/{repo}/merges/{number}/files
List the files a merge request changes, as a paginated summary without hunk
bodies. Use this for large diffs where the full diff field on the merge request
object is suppressed. Files are ordered alphabetically by new path (falling back
to old path for deletions). Requires read access to the repository.
Path parameters
| Name | Type | Description |
|---|---|---|
number |
integer | Per-repository merge-request number. |
Query parameters
| Name | Type | Default | Description |
|---|---|---|---|
page |
integer | 1 |
See Pagination. |
per_page |
integer | 50 |
Clamped to [1, 200]. |
Response
{
"files": [
{
"old_path": "internal/runner/poll.go",
"new_path": "internal/runner/poll.go",
"is_binary": false,
"is_add": false,
"is_delete": false,
"is_rename": false,
"additions": 18,
"deletions": 4,
"suppressed": false
}
],
"total": 12,
"page": 1,
"per_page": 50
}
A file is returned with suppressed: true and a suppressed_reason (for
example, a binary file) when its diff is omitted; the rest of the list is still
returned.
Status codes
| Status | When |
|---|---|
200 OK |
File summary returned (files may be empty for a zero-change merge request). |
400 Bad Request |
number is not an integer. |
404 Not Found |
Repository or merge request missing, or private and not readable. |
503 Service Unavailable |
The diff is unavailable (git backend down or comparison failed); the body carries suppressed_reason. |
Example
curl -H "Authorization: Bearer <token>" \
"https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/{number}/files?per_page=50"
GET /api/v1/repos/{owner}/{repo}/merges/{number}/files/{path}
Fetch the full structured diff for a single file in a merge request, including hunks and line numbers. The path is matched against the file's new path first, then its old path. Requires read access to the repository.
Path parameters
| Name | Type | Description |
|---|---|---|
number |
integer | Per-repository merge-request number. |
path |
string | File path within the repository (URL-encoded). |
Response
{
"old_path": "internal/runner/poll.go",
"new_path": "internal/runner/poll.go",
"is_binary": false,
"is_add": false,
"is_delete": false,
"is_rename": false,
"additions": 18,
"deletions": 4,
"suppressed": false,
"hunks": [
{
"old_start": 40,
"old_lines": 6,
"new_start": 40,
"new_lines": 8,
"lines": [
{ "op": " ", "content": "func poll() {", "old_no": 40, "new_no": 40 },
{ "op": "+", "content": " budget := newBudget()", "old_no": 0, "new_no": 41 }
]
}
]
}
In each line, op is " " (context), "+" (added), or "-" (removed);
old_no is 0 on added lines and new_no is 0 on removed lines. A binary or
oversized file is returned with suppressed: true and empty hunks.
Status codes
| Status | When |
|---|---|
200 OK |
File diff returned. |
400 Bad Request |
number is not an integer, the path is missing, or the path encoding is invalid. |
404 Not Found |
Repository or merge request missing (or not readable), or no file in the diff matches the path. |
503 Service Unavailable |
The diff is unavailable; the body carries suppressed_reason. |
Example
curl -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/{number}/files/internal%2Frunner%2Fpoll.go
GET /api/v1/repos/{owner}/{repo}/merges/{number}/comments
List the review comments on a merge request. Requires read access to the repository.
Path parameters
| Name | Type | Description |
|---|---|---|
number |
integer | Per-repository merge-request number. |
Response
[
{
"id": "<uuid>",
"mr_id": "<uuid>",
"author_id": "<uuid>",
"commit_sha": "9f1c2e0...",
"file_path": "internal/runner/poll.go",
"line_number": 41,
"body": "Consider a backoff here.",
"resolved": false,
"created_at": "2026-06-01T12:05:00Z",
"updated_at": "2026-06-01T12:05:00Z"
}
]
Status codes
| Status | When |
|---|---|
200 OK |
Comment list returned (possibly empty). |
400 Bad Request |
number is not an integer. |
404 Not Found |
Repository or merge request missing, or private and not readable. |
Example
curl -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/{number}/comments
POST /api/v1/repos/{owner}/{repo}/merges/{number}/comments
Create a review comment on a merge request. The body is stored as Markdown with HTML entities escaped. Requires write access to the repository.
Path parameters
| Name | Type | Description |
|---|---|---|
number |
integer | Per-repository merge-request number. |
Request body
{
"body": "Consider a backoff here.",
"commit_sha": "9f1c2e0...",
"file_path": "internal/runner/poll.go",
"line_number": 41
}
| Field | Type | Required | Description |
|---|---|---|---|
body |
string | yes | Comment text (Markdown). |
commit_sha |
string | no | Commit the comment anchors to. |
file_path |
string | no | File the comment anchors to. |
line_number |
integer | no | Line the comment anchors to. |
Response
201 Created with the new comment object.
Status codes
| Status | When |
|---|---|
201 Created |
Comment created. |
400 Bad Request |
Invalid body or missing body. |
404 Not Found |
Repository or merge request missing, or private and not readable. |
Example
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"body":"Consider a backoff here.","file_path":"internal/runner/poll.go","line_number":41}' \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/{number}/comments
PATCH /api/v1/repos/{owner}/{repo}/merges/{number}/comments/{cid}
Edit a review comment's body or toggle its resolved flag. Only the comment's author or a repository administrator may do this.
Path parameters
| Name | Type | Description |
|---|---|---|
number |
integer | Per-repository merge-request number. |
cid |
string | Comment UUID. |
Request body
{
"body": "Updated comment text.",
"resolved": true
}
Both fields are optional; send the ones you want to change.
Response
200 OK with the updated comment object.
Status codes
| Status | When |
|---|---|
200 OK |
Comment updated. |
400 Bad Request |
Invalid body, an invalid number, or an invalid comment id. |
403 Forbidden |
The caller is neither the comment author nor a repository administrator. |
404 Not Found |
Repository, merge request, or comment missing. |
Example
curl -X PATCH -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"resolved":true}' \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/{number}/comments/{cid}
DELETE /api/v1/repos/{owner}/{repo}/merges/{number}/comments/{cid}
Delete a review comment. Only the comment's author or a repository administrator may do this.
Path parameters
| Name | Type | Description |
|---|---|---|
number |
integer | Per-repository merge-request number. |
cid |
string | Comment UUID. |
Status codes
| Status | When |
|---|---|
204 No Content |
Comment deleted. |
400 Bad Request |
Invalid number or comment id. |
403 Forbidden |
The caller is neither the comment author nor a repository administrator. |
404 Not Found |
Repository, merge request, or comment missing. |
Example
curl -X DELETE -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/{number}/comments/{cid}
POST /api/v1/repos/{owner}/{repo}/merges/{number}/reviews
Submit a review decision for a merge request. A caller has at most one review;
submitting again replaces it. All reviews must be approved before the merge
request can be merged. Requires write access to the repository.
Path parameters
| Name | Type | Description |
|---|---|---|
number |
integer | Per-repository merge-request number. |
Request body
{
"state": "approved",
"body": "Looks good."
}
| Field | Type | Required | Description |
|---|---|---|---|
state |
string | yes | approved or changes_requested. |
body |
string | no | Review summary (Markdown). |
Response
{ "id": "<uuid>", "mr_id": "<uuid>", "state": "approved" }
Status codes
| Status | When |
|---|---|
201 Created |
Review recorded. |
400 Bad Request |
Invalid body, or state is not approved/changes_requested. |
404 Not Found |
Repository or merge request missing, or private and not readable. |
Example
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"state":"approved","body":"Looks good."}' \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/{number}/reviews
POST /api/v1/repos/{owner}/{repo}/merges/prepare-url
Validate a source/target branch pair and return a relative URL to the web new- merge-request form with the fields prefilled. This does not create a merge request. Requires write access to the repository.
Request body
{
"source_branch": "feature/retry-budget",
"target_branch": "main",
"title": "Add retry budget",
"description": "Markdown body.",
"draft": false
}
| Field | Type | Required | Description |
|---|---|---|---|
source_branch |
string | yes | Branch to merge from; must exist. |
target_branch |
string | yes | Branch to merge into; must exist. |
title |
string | no | Prefilled title. |
description |
string | no | Prefilled Markdown body. |
draft |
boolean | no | Prefill the draft flag. |
Response
{ "url": "/{owner}/{repo}/pulls/new?source_branch=...&target_branch=...", "expires_at": null }
Status codes
| Status | When |
|---|---|
200 OK |
URL returned. |
400 Bad Request |
Invalid body, or missing source_branch/target_branch. |
404 Not Found |
Repository missing, or private and not readable. |
422 Unprocessable Entity |
The source or target branch does not exist. |
Example
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{"source_branch":"feature/retry-budget","target_branch":"main"}' \
https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/merges/prepare-url
Errors
These endpoints use the shared error envelope and status-code conventions in
errors.md, including the 404-not-403 rule for private
repositories. Beyond the shared catalog, the merge action returns a small set of
stable 409 Conflict reason strings in the error field — draft_not_ready,
pending_reviews, and merge conflict — plus a required-pipeline rule's own
reason code when a merge or open is gated.
Rate limits
Read endpoints are metered as api.read and write endpoints (create, update,
merge, comment, review, prepare-url) as api.write, under the standard scope
budgets in rate-limits.md. The synchronous merge action runs
without the standard write-response deadline so a large merge is not cut off
mid-flight; it is still metered as api.write.