Skip to content

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.

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 revision
PUT /v1/sessions/{sessionId}/publications/{publicationId} open (or update) the pull request
GET /v1/sessions/{sessionId}/publications/{publicationId} watch it reach `succeeded`
PUT …/publications/{publicationId}/merges/{mergeId} merge that exact head
GET …/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.

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.

Terminal window
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.

Terminal window
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
└──▶ failed

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

Terminal window
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.

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:

  • status is waiting and lastError.code is merge_not_ready.
  • attempts increases. There is no attempt cap — a merge can wait as long as the pull request stays open and unchanged.
  • completedAt is 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.

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.