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.

~/agent
$ 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 .openapi
3.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.

~/agent
$ curl -si -X POST $API/v1/repos/acme/web/changes/12/merge -H "Idempotency-Key: merge-acme-web-12" …
HTTP/1.1 200 OK
RateLimit-Limit: 60
RateLimit-Remaining: 41
RateLimit-Reset: 23
idempotent-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.

packages/sdk
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.

ci · checks
$ bscope check list 12 --fail-on-required
CHECK STATUS SUMMARY
CI / build passed
CI / 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 .state
failed

Script your first change

Sign in with bscope auth login, open a change from your branch, and gate CI on its review.