API
Version 1: authentication, errors, paging, safe retries, rate limits, the SDK and every endpoint.
Everything the web app, the CLI and MCP do goes through Branchscope's HTTP API, version 1. Every path starts with /v1. The API is at http://<host>:3001 locally; any host names its address with GET <web>/.well-known/branchscope (api).
- The OpenAPI document:
GET /v1/openapi.jsondescribes every endpoint: its parameters, body, response and errors. Generate a client from it in any language. - The reference: every endpoint with an example, in the API reference.
- The TypeScript SDK:
@branchscope/sdk(below).
Authentication#
Send Authorization: Bearer <token>:
- a personal token (
bsc_pat_…); - an OAuth app's token (
bsc_oat_…for a person,bsc_ait_…for an installation), limited to its scopes; - or a Keycloak access token.
Without a credential you see only public repositories. A few things only a signed-in person in the web app can do, never a token, because they grant access: creating tokens, adding SSH keys, creating organizations and managing their members.
Versions#
/v1 is stable: fields and endpoints are added to it, never removed or changed. A change that would break callers comes as /v2.
Errors#
Every error is JSON with an HTTP status and a stable code:
{ "error": { "code": "guard_denied", "message": "main is a protected branch: agents can't update it", "details": { "message": "…" } } }
| Code | Status | Means |
|---|---|---|
invalid_request | 400 | a parameter or the body is wrong; details.issues lists each field |
unauthorized | 401 | sign in, or the credential isn't valid |
forbidden | 403 | not allowed |
guard_denied | 403 | the push guard refused a merge; details.message is its reason |
not_found | 404 | doesn't exist, or you can't see it (private repositories you can't read are not found) |
conflict | 409 | the state doesn't allow it, such as merging a closed change; a merge's conflicting files are in details.conflicts |
not_ready | 409 | not yet: a graph still building, a review still running |
idempotency_in_progress | 409 | a request with the same Idempotency-Key is still running |
gone | 410 | stored data is no longer there, such as a deleted graph |
idempotency_mismatch | 422 | the Idempotency-Key was used for a different request |
rate_limited | 429 | over the rate limit; wait for Retry-After |
internal | 500 | Branchscope's fault |
upstream_error | 502 | the git service couldn't be reached |
guard_unavailable | 503 | the push guard couldn't be reached, so the merge was refused |
Use the code, not the message: messages are for people and can change.
Lists#
Every list is paged. It returns { "data": [...], "nextCursor": "…" } and takes ?limit= (1-100, default 50) and ?cursor=. Pass nextCursor back as cursor for the next page; it's null on the last one. Cursors carry on where the last page stopped even as items are added, so nothing is shown twice.
| Filters | |
|---|---|
| changes | status (open, merged, closed), author, agent |
| activity and agent activity | repo, actor (a username), agent, since and until (ISO 8601 times) |
Retrying safely#
Creates and merges accept an Idempotency-Key header: any unique string, up to 200 characters. The first response to a key is kept for 24 hours, for your credential. Sending the same request with the same key gets that response back (with Idempotent-Replayed: true) instead of acting twice. So an agent that lost a connection can retry a merge without merging twice. A failed request frees its key, so it can be retried.
Rate limits#
| Who | Limit |
|---|---|
| each token | 1,000 requests an hour, and 60 a minute |
| anonymous callers | 60 requests an hour, per IP address |
Each token counts on its own, so an agent's token can't use up its person's. The web app makes several requests per page, so it has a larger budget for its users and visitors. Site operators can change the limits.
Every response says where you are: RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the window starts again). Over the limit is 429 rate_limited, with Retry-After in seconds.
The TypeScript SDK#
@branchscope/sdk is a typed client: one method per endpoint, typed from the same schemas as the API.
import { Branchscope, BranchscopeError } from "@branchscope/sdk";
const branchscope = new Branchscope({ baseUrl: "https://api.example.com", token: process.env.BRANCHSCOPE_TOKEN });
// Awaiting a list gives one page; for await walks every item across pages.
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);
}
- Errors are
BranchscopeError, withstatus,code,messageanddetails. - Retries: after a 429 it waits for
Retry-After. It retries server errors and network failures only where that can't act twice: reads,PUTandDELETE, and creates and merges, which it sends with an Idempotency-Key, the same on every retry.
It lives in the repository (packages/sdk) for now and isn't published to npm yet.
Endpoints#
Every path below starts with /v1. The API reference has each one's parameters, responses and an example.
You#
GET /me | who the token belongs to, its agent and scope, your organizations |
GET /tokens, POST /tokens, DELETE /tokens/{id} | your tokens; create (web session only) and revoke |
GET /ssh-keys, POST /ssh-keys, DELETE /ssh-keys/{id} | SSH keys (adding: web session only) |
PUT /me/profile, PUT, DELETE /me/avatar | your profile and avatar (web session only) |
GET /me/following, GET /me/notifications, POST /me/notifications/{id}/read, POST /me/notifications/read | what you follow, and your notifications; mark one or all read (web session only) |
GET /me/identities, DELETE /me/identities/{org} | your linked identities; unlink one (web session only) |
GET /me/passkeys, PUT, DELETE /me/passkeys/{id} | your passkeys: list them, rename one ({"label"}, 1–100 characters), remove one; refused (conflict) for the last way you sign in, upstream_error when Keycloak can't be reached (web session only). Adding one is Keycloak's own page, from Settings |
GET /namespaces/{name}, GET /namespaces/{name}/repos | a user or organization; its repositories |
Organizations#
POST /orgs, GET /orgs/{org}/members, POST /orgs/{org}/members, DELETE /orgs/{org}/members/{username} | organizations and their members |
PUT /orgs/{org}/profile, PUT, DELETE /orgs/{org}/avatar | an organization's profile and picture (owners, web session) |
PUT, DELETE /orgs/{org}/follow | follow or stop following it (web session) |
DELETE /orgs/{org} | delete it, with all its repositories, for good (owners, web session) |
GET, PUT, DELETE /orgs/{org}/sso, POST …/sso/domains, POST …/sso/domains/{domain}/verify, DELETE …/sso/domains/{domain} | its single sign-on: the provider, and the domains it verifies (owners, web session) |
Repositories#
GET /repos, POST /repos | list (with your access, open changes, last push) and create |
GET /repos?affiliation=&owner=&topic=&q=&sort= | affiliation: all, mine or following; by owner or topic; found by name or description; sort: name or pushed |
POST /repos/{owner}/{name}/fork | fork it ({owner, name, description, defaultBranchOnly, visibility}) |
PUT, DELETE /repos/{owner}/{name}/follow | follow it, or stop (web session) |
GET /repos/{owner}/{name} | branches, tags, clone URLs (cloneUrl, sshUrl), settings |
GET /repos/{owner}/{name}/analytics/languages | its languages by bytes at the default branch's head, and the commit they were counted at (the Analytics tab) |
DELETE /repos/{owner}/{name} | delete it and everything in it, for good (repository admins, signed in to the web UI) |
PUT /repos/{owner}/{name}/settings | visibility, description, topics, default branch, setup commands, install scripts, merge methods; protectedBranches, requireChanges and requiredChecks change the Protected branches ruleset |
PUT, DELETE /repos/{owner}/{name}/archive, POST …/transfer ({owner}) | archive or unarchive it; transfer it (signed in to the web UI) |
GET, PUT, DELETE /repos/{owner}/{name}/collaborators[/{username}] | collaborators |
GET, POST /repos/{owner}/{name}/branches, DELETE …/branches/{branch}, POST …/branches/{branch}/rename, POST …/branches/{branch}/restore | the branches; create, delete, rename, restore one |
GET /repos/{owner}/{name}/commits?rev= | history, newest first |
GET …/commits/{sha}, GET …/commits/{sha}/diff | one commit: its message, author, committer and parents; its diff |
GET /repos/{owner}/{name}/blame?ref=&path= | which commit last changed each line of a file |
GET /repos/{owner}/{name}/tree?rev=&path=, /blob?rev=&path= | files |
POST /imports, GET /imports/{id}, GET /repos/{owner}/{name}/import | imports: start one, its status and log |
GET, POST /repos/{owner}/{name}/rulesets[?kind=], GET, PUT, DELETE …/rulesets/{id}, POST …/rulesets/release-tags | branch and tag rulesets (administrators; never agents or apps) |
GET …/rulesets/{id}/insights | what a ruleset in evaluate mode would have refused |
GET, POST /repos/{owner}/{name}/mirrors, GET, PUT, DELETE …/mirrors/{id}, POST …/mirrors/{id}/sync | mirrors (administrators) |
GET /repos/{owner}/{name}/audit?action=, GET /orgs/{org}/audit?repositories=, GET /me/audit | the audit log, newest first; action keeps one action (secret.set) or those starting with it (secret); an organization's has its repositories' entries too, unless repositories=false. Administrators and owners as people, never agents or apps; yours signed in to the web UI |
GET, POST /external-tokens, GET, PUT, DELETE /external-tokens/{id}, POST …/{id}/reveal | your external tokens (signed in to the web UI) |
Changes and reviews#
GET /repos/{owner}/{name}/changes, POST …/changes | list, open ({title, description, source, target}) |
GET …/changes/{number} | the change, its commits, conflicts and review status |
GET …/changes/{number}/diff | the textual diff |
POST …/changes/{number}/merge, /close, /reopen, /review | act on it; review re-runs the graph review. An administrator merges past failing required checks with {overrideReason} |
GET …/changes/{number}/views | the graph review: changeMap and api view documents, checks (rules, plan), renamed, readable names |
GET …/changes/{number}/connections | other repositories: what the change touches, the functions on the other side, breaking, missing and affected findings, each repository's pinned commit |
GET …/changes/{number}/conflicts | the conflict map: status (none until the target moves past the merge base), the conflicts document (elements both sides touched, conflicting files and their regions, semantic conflicts), both sides' maps, readable names |
GET, POST …/changes/{number}/comments, PUT, DELETE …/comments/{id} | the discussion: comments and replies ({body, parentId}); edit or delete your own |
POST …/changes/{number}/status | an agent's status on the change ({status, text}: working, pushed, blocked, done) |
Checks and runs#
GET, POST /repos/{owner}/{name}/commits/{sha}/checks | a commit's checks; report one ({name, status, summary, detailsUrl}; apps need checks:write) |
GET …/changes/{number}/checks | a change's checks against the required ones: state is passed, pending, failed or none |
GET /repos/{owner}/{name}/runs?change=, GET …/runs/{id} | workflow runs, newest first, and one run with its jobs |
POST …/runs ({workflow, ref}), POST …/runs/{id}/cancel, /rerun | run a workflow by hand, cancel a run, run it again |
POST …/runs/{id}/approve | give a run its withheld secrets and identity tokens, so its jobs that need them run (administrators, as people) |
POST …/runs/{id}/review ({decision, comment, environments}), POST …/runs/{id}/start-now | approve or reject the run's deployments waiting for review (listed reviewers); end its wait timers now (administrators); as people |
GET …/runs/{id}/artifacts/{artifact} | download an artifact, as <name>.tar.gz |
GET /repos/{owner}/{name}/actions/caches, DELETE …/actions/caches[?branch=] | the repository's caches; clear them, or one branch's (administrators, as people) |
GET …/runs/{id}/jobs/{job}/log?after= | a job's log from a character offset; ask again from next to follow it until done |
GET /orgs/{org}/runners, POST …/runners/registration-tokens, DELETE …/runners/{id} | an organization's runners; a registration token (owners, web session); remove one |
GET /repos/{owner}/{name}/secrets, PUT, DELETE …/secrets/{secret}; the same under /orgs/{org}/secrets and /repos/{owner}/{name}/environments/{environment}/secrets | secrets for runs, by name, and an environment's; values are never returned (people only, never agents or apps) |
GET /workflow-schema.json | the workflow format's JSON Schema (no sign-in) |
Releases and deployments#
GET, POST /repos/{owner}/{name}/releases, GET, PUT, DELETE …/releases/{tag} | releases; make one ({tag, target, title, notes, previousTag, draft, prerelease}), change it ({"draft": false} publishes it), delete it; agents and apps only draft |
POST …/releases/generate-notes, GET …/releases/{tag}/views | the notes offered for a tag; its change map (apps need reviews:read) |
GET /repos/{owner}/{name}/compare/{range}/views | the change map between two commits, as a new release's (apps need reviews:read) |
GET /repos/{owner}/{name}/environments, GET, PUT …/environments/{environment} | environments, with what's running in each; set one's required tag, reviewers, Prevent self-review and wait timer ({requiredTag, reviewers, preventSelfReview, waitTimerMinutes}; administrators, never agents) |
GET …/environments/{environment}/secrets, PUT, DELETE …/secrets/{secret} | an environment's secrets (administrators, as people) |
GET, POST /repos/{owner}/{name}/deployments, GET …/deployments/{id} | deployments; record one ({environment, ref, status, url, description}; apps need deployments:write); one with its history |
POST …/deployments/{id}/statuses, POST …/deployments/{id}/redeploy | set its status; Deploy again (apps need deployments:write) |
POST …/deployments/{id}/review ({decision, comment}) | approve or reject one waiting for your review (listed reviewers, as people) |
Agents and tasks#
GET /repos/{owner}/{name}/agents | the agent apps installed on the repository |
GET, POST /repos/{owner}/{name}/tasks | tasks for them; hand one out ({agent, instructions, change} or {agent, instructions, base}) |
GET /tasks, GET /tasks/{id} | an agent app's open tasks (a person's: the ones they handed out); one task |
POST /tasks/{id}/status, POST /tasks/{id}/cancel | the agent reports ({status, text, change}); a person cancels |
GET /orgs/{org}/agents | an organization's agent apps and what they're doing |
PUT /orgs/{org}/installations/{slug} | an installed app's repositories, maxOpenChanges, and paused |
Graphs and views#
GET /repos/{owner}/{name}/graph?ref= | a branch's or commit's graph build, with its status |
GET …/graphs, POST …/graphs ({ref}) | builds; start one |
GET …/graphs/{commit} | the graph as canonical JSON |
GET …/views/architecture?ref=&project= | the architecture view (inside one project with project) |
GET …/views/data-flow?ref= | the personal-data flow view |
GET /orgs/{org}/graph, GET /users/{username}/graph (?view=&against=) | the organization graph: services across an organization's repositories, or a person's |
GET, POST /orgs/{org}/graph/views, PUT, DELETE …/graph/views/{slug}; the same under /users/{username} | its saved views (changing them: owners, as people) |
GET /orgs/{org}/connections/services, GET /users/{username}/connections/services | the service names used across the repositories |
Webhooks#
GET, POST /repos/{owner}/{name}/webhooks | a repository's webhooks; add one (its secret comes back once) |
GET, POST /orgs/{org}/webhooks | an organization's webhooks |
GET, PUT, DELETE /webhooks/{id} | a webhook: its URL, events, whether it's active |
POST /webhooks/{id}/rotate-secret, POST /webhooks/{id}/ping | a new secret (shown once); a test event |
GET /webhooks/{id}/deliveries, POST …/deliveries/{delivery}/redeliver | the delivery log; send one again |
Integrations#
GET, POST /repos/{owner}/{name}/integrations, GET, POST /orgs/{org}/integrations | a repository's or organization's integrations; add one |
GET, PUT, DELETE /integrations/{id} | an integration: its service, events, whether it's active |
POST /integrations/{id}/test, GET …/deliveries, POST …/deliveries/{delivery}/redeliver | a test message; the delivery log; send one again |
GET, PUT, DELETE /orgs/{org}/linear | the organization's Linear connection; connect with an API key; disconnect |
POST /orgs/{org}/linear/oauth, POST …/linear/oauth/callback | connect with the server's Linear app: where to send the person, then finish with what Linear sent back |
PUT …/linear/inbound, GET …/linear/events | the signing secret of a Linear webhook made by hand; the last 7 days' events from Linear |
PUT …/linear/settings, POST …/linear/refresh | its automations and each team's states; read the teams and states again |
GET, POST …/changes/{number}/issues, DELETE …/issues/{identifier} | a change's linked Linear issues; link or unlink one (write access) |
GET /repos/{owner}/{name}/ai-keys, PUT, DELETE …/ai-keys/{app}/{provider}; the same under /orgs/{org}/ai-keys | AI provider keys for installed apps (administrators and owners, as people) |
POST /installations/ai-keys/{provider}, POST …/{provider}/usage | an installed app gets the key for the repository it works on; reports what it spent |
Integrations, Linear and AI provider keys are managed by people only, with a session or a personal write token. Linear's events come in at /linear/webhook, outside /v1.
OAuth apps#
GET, POST /apps, GET, PUT, DELETE /apps/{slug}, GET /apps/{slug}/settings | the OAuth apps you manage; register, change or delete one (web session) |
POST /apps/{slug}/rotate-secret, PUT, DELETE, GET /apps/{slug}/icon | a new client or webhook secret; its icon |
GET /authorizations, DELETE /authorizations/{slug} | the apps you've authorized to act for you; revoke one |
GET, POST /orgs/{org}/installations, PUT, DELETE …/installations/{slug} | the apps installed in an organization; install, change or remove one (owners) |
Tokens are issued at POST /oauth/token and revoked at POST /oauth/revoke, outside /v1; see OAuth apps.
Chats#
GET, POST /chats | your chats, newest first, with unread counts; open (or find) the one with someone |
GET /chats/{id}, GET, POST /chats/{id}/messages | a conversation; its messages, newest first; send one |
PUT, DELETE /chats/{id}/messages/{message} | edit or delete your own message |
POST /chats/{id}/read, POST /chats/{id}/typing | mark it read up to a message; say you're typing |
POST /chats/{id}/messages/{message}/report | report a message to the site administrators |
GET /me/blocks, PUT, DELETE /me/blocks/{username} | the people you blocked; block or unblock someone |
GET /people?q= | people to chat with, by username or name |
GET /events | your live events, as Server-Sent Events |
Everything else#
GET /activity | push and merge events you can see |
GET /activity/agents | agents' MCP sessions and writes you can see |
POST, GET, DELETE /mcp | the MCP server (agents only: OAuth apps and installed agent apps) |
GET /guard, PUT /guard | push guard state and deny-all (site admins) |
GET /admin/hosted-runners, POST …/registration-tokens, PUT, DELETE …/{id}; GET /admin/hosted-access, PUT, DELETE …/{namespace} | hosted runners and who may use them (site admins) |
GET /ssh | the SSH URL and host key fingerprints |
GET /openapi.json | this API's OpenAPI document |
GET /health (liveness) is outside /v1.
View documents#
Views are JSON documents (kind: "view", schema urn:branchscope:view). Each holds diagrams of:
- groups: projects, files, types;
- nodes: with label, kind, status (
added,removed,changed,renamed,context), notes and tags; - edges: with label, status, whether resolved, and the rule effect and IDs they break.
The web app draws them; agents can read them directly.
Graph documents#
Graphs, diffs and checks are versioned JSON (schema 0.4.x) in canonical form. Their exact shape is specified in docs/graph/SPEC.md in the repository, and their JSON Schemas are in the OpenAPI document.