Open a pull request
After this page you can read what an agent changed, open a pull request from it, wait for review and branch policy, and merge exactly the commit that was reviewed — or fail loudly instead of merging something else.
The shape of the flow
Section titled “The shape of the flow”Publishing is two separate durable commands, not one. Opening a pull request and integrating its reviewed head have different authority, different retry behavior and different failure modes, so they are different resources.
GET /v1/sessions/{sessionId}/repository/review read the diff, get a revisionPUT /v1/sessions/{sessionId}/publications/{publicationId} open (or update) the pull requestGET /v1/sessions/{sessionId}/publications/{publicationId} watch it reach `succeeded`PUT …/publications/{publicationId}/merges/{mergeId} merge that exact headGET …/publications/{publicationId}/merges/{mergeId} watch it reach `succeeded`Both PUTs take an identifier you mint, so a timeout or a retry addresses
the same command instead of creating a second pull request. The prerequisites
are a session whose source is a GitHub grant that includes PUBLISH, and an
installation holding wamp.cloud.publications:create and
wamp.cloud.publications:merge.
Read the review first
Section titled “Read the review first”The review is not optional decoration. It produces the revision token that
the publish call requires, and that token is how the server knows you published
what you actually looked at.
curl -sS "$WAMP_API/v1/sessions/$SESSION_ID/repository/review" \ -H "Authorization: Bearer $WAMP_TOKEN"{ "review": { "branch": "wamp/invoice-pagination", "baseRef": "origin/main", "revision": "4f9c0b2e7a1d6c3b8e5f0a2d4c6b8e1f3a5c7d9b0e2f4a6c8b0d2e4f6a8c0b2d", "files": [ { "path": "src/invoices/list.ts", "status": "modified", "additions": 42, "deletions": 6, "binary": false, "patch": "@@ -1,6 +1,42 @@\n…" } ], "additions": 42, "deletions": 6, "patchTruncated": false }}The contract guarantees revision and files; the fields above are what the
server sends today, and it may add more.
| Field | Meaning |
|---|---|
revision |
64 hex characters. A SHA-256 over the reviewed tree — not a git commit SHA. |
branch |
The working branch inside the sandbox. |
baseRef |
What the diff is against. |
files[].status |
added, modified, deleted or renamed. oldPath is present on a rename. |
files[].patch |
Omitted for binary files and when the patch budget is exhausted. |
patchTruncated |
true when patches were dropped to stay inside the response budget. The file list is still complete. |
If the review is refused, this endpoint reports repository-level problems
directly: 409 no_changes when the agent changed nothing, 409 not_repository
when the session has no checkout, 413 review_too_large, and 409 review_changed when the tree moved while the review was being computed. Those
responses use a { "success": false, "error": "<code>" } body rather than the
standard error shape — see Errors.
Open the pull request
Section titled “Open the pull request”PUBLICATION_ID=$(uuidgen | tr 'A-Z' 'a-z')
curl -sS -X PUT \ "$WAMP_API/v1/sessions/$SESSION_ID/publications/$PUBLICATION_ID" \ -H "Authorization: Bearer $WAMP_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "revision": "4f9c0b2e7a1d6c3b8e5f0a2d4c6b8e1f3a5c7d9b0e2f4a6c8b0d2e4f6a8c0b2d", "title": "Paginate the invoices endpoint", "body": "Adds cursor pagination.\n\nOpened by an agent run.", "draft": true }'| Field | Required | Constraint |
|---|---|---|
revision |
yes | Exactly ^[a-f0-9]{64}$, from the review |
title |
yes | 1–256 characters, no CR or LF |
body |
yes | Up to 65 536 characters. May be "" |
draft |
no | Boolean, defaults to true |
draft: true is the default deliberately: an agent’s work arrives as a draft
unless you decide otherwise. A draft pull request cannot be merged, so if you
intend to merge in the same flow you must publish with draft: false.
The response is 201 the first time and 200 on replay, both with a
Location header and the publication resource:
{ "publication": { "id": "…", "sessionId": "…", "status": "admitted", "revision": "4f9c…", "title": "Paginate the invoices endpoint", "createdAt": "2026-08-11T09:12:00.000Z", "updatedAt": "2026-08-11T09:12:00.000Z" }}admitted means the command is durable, not that a pull request exists. The
server then commits, pushes and calls GitHub in the background:
admitted ──▶ prepared ──▶ attempted ──┬──▶ succeeded └──▶ failedOnly succeeded and failed are terminal, and result appears only on
success:
{ "publication": { "status": "succeeded", "result": { "branch": "wamp/invoice-pagination", "commitSha": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d", "pullRequest": { "number": 412, "url": "https://github.com/acme/payments-api/pull/412", "draft": true, "reused": false } }, "completedAt": "2026-08-11T09:12:07.512Z" }}pullRequest.reused: true means an existing pull request for that branch was
updated rather than a new one opened. Treat it as normal — it is what makes a
second publication from the same session idempotent at the GitHub level rather
than spamming the repository.
You can watch for the terminal state either by polling
GET …/publications/{publicationId} or by following the session’s event log,
which carries wamp.publication.created on success and
wamp.publication.failed (with data.errorCode) on failure. Following events
is cheaper if you are already following the run — see
Follow a run live.
Merge exactly what was reviewed
Section titled “Merge exactly what was reviewed”MERGE_ID=$(uuidgen | tr 'A-Z' 'a-z')
curl -sS -X PUT \ "$WAMP_API/v1/sessions/$SESSION_ID/publications/$PUBLICATION_ID/merges/$MERGE_ID" \ -H "Authorization: Bearer $WAMP_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "strategy": "squash", "mode": "when_ready" }'| Field | Default | Values |
|---|---|---|
strategy |
squash |
merge, squash, rebase |
mode |
now |
now, when_ready |
commitTitle |
— | 1–256 characters, no CR or LF |
commitMessage |
— | Up to 65 536 characters |
Notice what is not in that body: the head to merge. The server takes
expectedHeadSha from the publication and merges that exact commit. If someone
pushed to the pull request after your review, the merge fails rather than
integrating commits nobody looked at. This is the single most important property
of the endpoint, and it is not configurable.
admitted ──▶ attempted ──┬──▶ succeeded └──▶ waiting ────┤ (mode: when_ready) └──▶ failed{ "merge": { "id": "…", "publicationId": "…", "status": "succeeded", "strategy": "squash", "mode": "when_ready", "pullRequestNumber": 412, "expectedHeadSha": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d", "mergedCommitSha": "9f8e7d6c5b4a39281706f5e4d3c2b1a098765432", "attempts": 2, "completedAt": "2026-08-11T09:31:44.108Z" }}mergedCommitSha is present exactly when status is succeeded — a database
constraint guarantees the equivalence, so its presence is a sound success test.
At most one merge per publication can ever succeed, so a duplicate merge
attempt cannot double-integrate.
Waiting on branch policy
Section titled “Waiting on branch policy”mode: "now" means attempt once and fail if GitHub says the pull request is
not mergeable. That is the right choice when a human is watching your UI.
mode: "when_ready" is for the common real case: required status checks are
still running, or a review is outstanding. The merge parks in waiting, and the
server re-attempts roughly every 30 seconds until GitHub accepts it. While it
waits:
statusiswaitingandlastError.codeismerge_not_ready.attemptsincreases. There is no attempt cap — a merge can wait as long as the pull request stays open and unchanged.completedAtis absent.
That means your side owns the timeout. Decide how long a merge may wait (your own SLA, not the server’s), and if it elapses, tell the customer and stop polling. There is no endpoint that cancels a waiting merge; what ends it is the pull request itself changing — a push moves the head and the merge fails, and closing the pull request fails it too.
Polling a when_ready merge that is not yet mergeable can also answer
409 cloud_publication_merge_not_ready. Treat that as “still waiting”, honor
any Retry-After, and keep the same merge id.
When a merge is rejected
Section titled “When a merge is rejected”A failed merge carries a diagnostic code:
{ "merge": { "status": "failed", "lastError": { "code": "pull_request_changed" }, "attempts": 1 } }lastError.code |
What happened | What to do |
|---|---|---|
pull_request_changed |
The head moved after the publication recorded it. | Re-review, publish again to update the pull request, then merge the new head with a new merge id. |
pull_request_closed |
Someone closed it. | Nothing to merge. Surface it and stop. |
merge_not_ready (with mode: now) |
Checks pending, review missing, or the pull request is still a draft. | Retry with mode: when_ready, or publish with draft: false first. |
merge_not_allowed |
GitHub refused the merge for this actor or this strategy — commonly branch protection, or a strategy the repository disallows. | Change strategy, or ask the repository administrator. Retrying unchanged will not help. |
repository_read_only |
The grant behind the session no longer permits publishing. | Ask the administrator to re-grant with PUBLISH; see Connect a repository. |
github_unavailable |
GitHub was unreachable or erroring. | Transient. The server retries this itself; it does not fail the merge. |
Other codes can appear as the failure surface grows. Treat an unrecognized code as a permanent failure of that merge, report it verbatim to whoever can act on it, and do not silently retry — a merge that failed for an unknown reason is exactly the case where guessing is expensive.
Because the merge id is yours, retrying the same command is always safe: the same id with the same body returns the existing merge rather than starting a second one. To merge a genuinely different head after a re-publish, mint a new merge id.
Related
Section titled “Related”- Publications operations — the generated endpoint reference, including every documented response.
- Connect a repository — grants, and why
PUBLISHmatters. - Events reference —
wamp.publication.*payloads. - Errors — the
409conflict semantics behind bothPUTs.