API, SDK and CLI
Everything the web app does, from your own code
The web app, the CLI and MCP all work through one HTTP API. Call it from a script, a CI job or an agent, with errors you can act on, retries that can't act twice, and a CLI that prints text or JSON.
REST
One API, version 1, that doesn't break
Every path starts with /v1, and fields and endpoints are only ever added to it; a change that would break callers would come as /v2. GET /v1/openapi.json describes every endpoint, its parameters, body, responses and errors, to generate a client in any language. Every error is JSON with a stable code, such as guard_denied when the push guard refused a merge.
$ curl -s -X POST $API/v1/repos/acme/web/changes/12/merge -H "Authorization: Bearer $TOKEN"{ "error": { "code": "guard_denied", "message": "main is a protected branch: agents can't update it","details": { "message": "main is a protected branch: agents can't update it" } } }$ curl -s $API/v1/openapi.json | jq -r .openapi3.1.1
Safe retries
Retry without merging twice
Creates and merges take an Idempotency-Key: sent again with the same key, a request gets its first answer back, marked idempotent-replayed, instead of acting twice. Each token has its own rate limit, by default 1,000 requests an hour and 60 a minute, so an agent can't use up its person's. Every answer says where you are in RateLimit-* headers, and over the limit says when to try again.
$ curl -si -X POST $API/v1/repos/acme/web/changes/12/merge -H "Idempotency-Key: merge-acme-web-12" …HTTP/1.1 200 OKRateLimit-Limit: 60RateLimit-Remaining: 41RateLimit-Reset: 23idempotent-replayed: true
TypeScript SDK
A typed client, from the same schemas
@branchscope/sdk has a method for every endpoint, typed from the schemas the API itself checks requests against. Lists page themselves with for await, errors are BranchscopeError with the API's code, and it retries only what can't act twice. It lives in the repository, in packages/sdk, and isn't published to npm.
import { Branchscope, BranchscopeError } from "@branchscope/sdk";
const branchscope = new Branchscope({ baseUrl, token });
for await (const change of branchscope.changes.list({ owner: "acme", name: "web", status: "open" })) {
console.log(change.number, change.title);
}
try {
await branchscope.changes.merge({ owner: "acme", name: "web", number: 12 });
} catch (error) {
if (error instanceof BranchscopeError && error.code === "guard_denied") console.log(error.message);
}CLI
bscope, for terminals, scripts and CI
bscope works with repositories, changes, reviews, checks, runs, releases, deployments, webhooks and secrets. Every command prints readable text, or the API's own JSON with --json, and its exit code says what happened: 3 when change review --fail-on deny finds a broken rule or check list --fail-on-required a required check that hasn't passed, 4 when you're not signed in or not allowed.
$ bscope check list 12 --fail-on-requiredCHECK STATUS SUMMARYCI / build passedCI / test (node: 24) failed required test failed (exit 1)Required checks failed: CI / test (node: 24).bscope: required checks haven't passed (failed)$ echo $?3$ bscope check list 12 --json | jq -r .statefailed
Script your first change
Sign in with bscope auth login, open a change from your branch, and gate CI on its review.