GitHub's async merge API: how to merge stacked pull requests from your bot

GitHub’s async merge API has been GA since October 1, 2026, and is the only programmatic way to merge stacked pull requests. Your automation sends a PUT to merge-async, receives a UUID, and polls the result: the merge runs in the background, not within a single request. If you maintain a merge bot, a release script, or an agent that integrates PRs, this changes what “done” means.

GitHub presents the async API as the recommended path for programmatic merges, instead of the synchronous REST endpoint and GraphQL mutations. Recommended is not the same as deprecated: at the time of publication, the synchronous PUT …/merge endpoint remained documented and functional. But the use case for stacked PRs only exists in the new one. In GitHub’s own words: “It’s also the only merge API that supports stacked pull requests.”

What are stacked pull requests?

Stacked pull requests are an ordered chain of small PRs in the same repository, where each one points to the branch of the PR below it, and the entire chain ends in a single base branch, typically main. Instead of a 2,000-line PR that no one wants to review, you open three or four focused layers that different reviewers can work on in parallel.

GitHub launched native stacks in public preview on July 30, 2026. You can create them on github.com, from GitHub Mobile, or with the CLI extension github/gh-stack (the gh stack command), which is also distributed as a skill for agents, so coding agents can build stacks on their own. When you merge a layer, all unintegrated layers below it enter the same operation; PRs above remain open and re-point themselves automatically.

Two time-sensitive warnings:

  • The async merge API is GA; stacked PRs themselves remained in public preview at the time of publication.
  • In its July announcement, GitHub indicated that merge queue support for stacks would be rolled out progressively in the following weeks. Check that your repository has it before depending on merge_queue for a stack.

What does GitHub’s async merge API change?

It splits the merge into two calls: one to request it and another to learn the result.

Synchronous (PUT …/pulls/{n}/merge) Asynchronous (PUT …/pulls/{n}/merge-async)
When the merge runs Within the request In the background
What it returns to you merged: true/false and a SHA A status and a UUID to poll
Stacked PRs Not supported Supported (also integrates the layers below)
Merge queue — merge_action: merge_queue
Complex merges Can time out Designed to avoid timeouts; some errors can be retried

The second endpoint is GET /repos/{owner}/{repo}/pulls/{pull_number}/merge-async/{uuid}, which returns the current result of a request you already made.

How do you merge a pull request with GitHub’s API?

You send a PUT with the same fields you already know from the synchronous endpoint, plus one new one:

  • commit_title and commit_message
  • sha: the SHA of the head the PR must match
  • merge_method: merge, squash, or rebase
  • merge_action (the new one): default, direct_merge, or merge_queue. With default, GitHub chooses the most appropriate path.
curl -L -X PUT \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "X-GitHub-Api-Version: 2026-03-10" \
  https://api.github.com/repos/OWNER/REPO/pulls/PULL_NUMBER/merge-async \
  -d '{"sha":"HEAD_SHA","merge_method":"squash","merge_action":"default"}'

The HTTP status code from that first call already tells you a lot:

  • 202: the request was accepted and the merge will run in the background. Save details.uuid.
  • 200: the PR was already integrated or is already in a merge queue.
  • 409: an async merge request for this PR already exists. GitHub returns the UUID of that request, and its options may differ from the ones you just sent.
  • 400: the PR cannot be merged right now, for example because it’s closed or still a draft.

The exact token permissions are in the official API reference; check them there before granting access to your bot.

What do pending, merged, enqueued, and failed mean?

Each response includes a status with one of four values, and a correct bot treats each as a distinct outcome:

status What it means What your bot should do
pending Accepted, not finished Continue polling with backoff
merged Done; the response includes the SHA of the merge commit Log the SHA and proceed
enqueued Handed off to the merge queue, not yet integrated Stop polling the async record and verify the final PR status separately
failed The merge did not happen; details.message explains why Show the message to a person or apply your retry policy

The trap is enqueued. It looks like success, and a bot that announces “merged” at that point is wrong every time the queue rejects the PR later. To confirm the final merge, use the existing endpoint GET /repos/{owner}/{repo}/pulls/{pull_number}/merge, which responds 204 when the PR is integrated and 404 while it is not.

How do you migrate your merge bot to the async API?

Replace “one call and check merged” with a small state machine. This outline is built only from the documented request and response contract and was not run against a real repository: test it first in a disposable repository.

#!/usr/bin/env bash
set -euo pipefail
API="https://api.github.com/repos/$OWNER/$REPO/pulls/$PR"
H=(-H "Accept: application/vnd.github+json"
   -H "Authorization: Bearer $GITHUB_TOKEN"
   -H "X-GitHub-Api-Version: 2026-03-10")

# 1. Request the merge, pinned to the SHA of the head you reviewed
out=$(curl -sS -L -X PUT "${H[@]}" -w '\n%{http_code}' "$API/merge-async" \
  -d "{\"sha\":\"$HEAD_SHA\",\"merge_method\":\"squash\",\"merge_action\":\"default\"}")
code=$(tail -n1 <<<"$out"); body=$(sed '$d' <<<"$out")

case "$code" in
  400) echo "Cannot merge: $(jq -r '.details.message' <<<"$body")"; exit 1 ;;
  200) echo "Already resolved: $(jq -r '.status' <<<"$body")"; exit 0 ;;
  409) echo "Request already exists; check its options:"; jq '.details' <<<"$body" ;;
esac
uuid=$(jq -r '.details.uuid' <<<"$body")

# 2. Poll until reaching a terminal state
delay=5
while true; do
  out=$(curl -sS -L "${H[@]}" -w '\n%{http_code}' "$API/merge-async/$uuid")
  code=$(tail -n1 <<<"$out"); body=$(sed '$d' <<<"$out")
  [ "$code" = "404" ] && { echo "Result expired or not found"; exit 1; }
  case "$(jq -r '.status' <<<"$body")" in
    merged)   echo "Merged: $(jq -r '.details.sha' <<<"$body")"; exit 0 ;;
    enqueued) echo "In merge queue; check final status with GET $API/merge"; exit 0 ;;
    failed)   echo "Failed: $(jq -r '.details.message' <<<"$body")"; exit 1 ;;
    pending)  sleep "$delay"; delay=$(( delay < 60 ? delay * 2 : 60 )) ;;
  esac
done

GitHub doesn’t document a polling interval; exponential backoff with a 60-second cap is a reasonable value, not a requirement.

If you’re already merging PRs from the terminal, this flow aligns with what we saw in Copilot CLI introduces tabs for issues, PRs and gists without leaving the terminal.

What errors should you avoid with the async merge API?

  • A 202 is not approval. At the time of the request, GitHub only performs basic PR state checks. Branch protection rules and repository rules are evaluated when the merge actually executes, so an accepted request can end up failed.
  • Always send sha. If you omit it, GitHub uses the head at the time of the request, and the merge gets canceled if someone pushes in between. Sending the SHA you reviewed makes that rule explicit, which is what you want in automation.
  • Results expire. According to the API reference at the time of publishing this note, an async result is kept for 24 hours from its last update; after that, its UUID returns 404. Don’t use the UUID as a permanent audit record: save the result yourself.
  • Stacks merge downward. Requesting a merge of layer 3 merges layers 1, 2, and 3. Make sure your bot’s idea of “which PR am I merging” matches that.
  • Bypassing rules isn’t the default behavior. The announcement mentions you can optionally skip rules if you have permission. Keep your bot on the protected path and reserve that exception for a deliberate decision by a person.

Should you migrate now?

Yes, if your automation merges large PRs or in repositories with high activity, or if your team starts using stacks, especially those generated by agents. The migration is two endpoints and four states. Teams that never do programmatic merges lose nothing by waiting; those that do will want explicit handling of terminal states anyway, because that’s what makes a bot’s “merged” actually mean merged.