API overview
The REST API is what the web console and the CLI use. Everything they can do is available to your own scripts. The reference is split into pages:
Base URL
Section titled “Base URL”https://<any node>:7080/v1Every node serves the API on port 7080 with the same TLS certificate as the console. In a cluster, any node accepts any request and passes it on to the node that holds the resource. The certificate is self-signed; download it from https://<node>:7080/tls.pem and trust it, or pass -k to curl while you try things out.
Requests and responses are JSON (Content-Type: application/json). Field names are snake_case, times are RFC 3339 in UTC, sizes are bytes, and durations are Go-style strings such as 72h or 30m. A request body may be at most 2 MiB, and a field the API does not know is rejected with 400 invalid_request.
Names of branches and bases use lowercase letters, digits and single hyphens, start with a letter, and have at most 32 characters (feature-x). A runtime is written engine@version, for example postgres@18.6.
Three paths are outside /v1 and need no login: GET /healthz (always ok), GET /readyz (503 until the node is initialized and has a cluster leader) and GET /tls.pem.
Authentication
Section titled “Authentication”Send an API token in the Authorization header:
curl -sk -H "Authorization: Bearer dbb_..." https://node1:7080/v1/branchesThere are two kinds of token. Both start with dbb_, are shown once when they are created, and only their hash is stored.
| Kind | Created by | Acts as | Scope |
|---|---|---|---|
| Service token | An admin: Settings → Service tokens, db-branch token create, or POST /v1/tokens |
The token itself (for CI and integrations) | branches (default) or full |
| Personal token | Any signed-in person: Account → MCP access, or POST /v1/account/tokens |
The person who created it | branches |
The scope limits what a token can do:
branchesmay create, use and delete branches and read most other resources. It cannot change bases, runtimes, settings or tokens, and it cannot read settings, tokens, route users, backups or a base’s refresh source, because those can hold secrets.fullmay do everything an admin can.
A token that is invalid, expired or revoked gets 401 unauthenticated. A token cannot create other tokens.
The console does not use tokens. It signs in with POST /v1/auth/login and receives a dbb_session cookie (HttpOnly, Secure, SameSite=Strict). Every request other than GET must then carry the session’s X-CSRF-Token header, whose value is csrf_token in the login response, or it fails with 403 csrf_failed. Scripts should use tokens instead. The CSRF check does not apply to requests with a bearer token.
Without any credentials, only a few calls work: creating a branch from a public snapshot, reading and connecting to an open branch, reading an operation, and listing public bases. See Branches.
The db-branch command talks to the agent over a local UNIX socket instead. Whoever can open the socket (root or the db-branch group) acts as an admin, and a few calls work only there: POST /v1/auth/reset, POST /v1/cluster/token and POST /v1/backups/restore return 403 socket_only over HTTPS.
Which calls are allowed is decided per request by an action name (branch.create, base.delete, and so on), shown for each endpoint in this reference. The built-in login is a single admin, who may do everything. Paid modules such as auth add roles and map them to these actions.
Errors
Section titled “Errors”A failed request returns a status code and a body of this shape:
{ "error": { "code": "not_found", "message": "branch feature-x: not found" }}Branch on code. The message is for people and may change.
| Status | code |
Meaning |
|---|---|---|
| 400 | invalid_request |
Malformed JSON, an unknown or missing field, or an invalid name, runtime, TTL or setting |
| 400 | weak_password |
The new password is too weak |
| 401 | unauthenticated |
No session or token, or it has expired |
| 401 | invalid_credentials |
Wrong user name or password |
| 403 | forbidden |
The action is not allowed for this caller or token scope |
| 403 | csrf_failed |
Cookie session without a matching X-CSRF-Token |
| 403 | socket_only |
Works only on a node’s local socket |
| 404 | not_found |
No such resource |
| 404 | runtime_not_installed |
The runtime is not installed on that node |
| 409 | already_exists |
The name is taken |
| 409 | conflict |
The resource is not in a state that allows this (for example resetting a branch that is not ready) |
| 409 | in_use |
Something still uses it (a base with branches, a runtime in use) |
| 409 | unclean_shutdown |
The base server did not stop cleanly, so no snapshot was taken |
| 409 | other_node |
The work belongs to another node |
| 409 | rollout_running |
A cluster update is already in progress |
| 422 | runtime_unavailable |
The runtime version is not offered |
| 422 | query_failed |
The SQL statement failed |
| 429 | rate_limited |
Too many login attempts; see the Retry-After header |
| 503 | no_leader |
The cluster has no leader at the moment; retry after the Retry-After header |
| 503 | no_capacity |
No node has room for the branch |
| 503 | cluster_upgrading |
A cluster update is changing the protocol |
| 503 | not_initialized |
The node has not run db-branch init, or the feature is not set up on it |
| 503 | modules_pending |
This node does not yet run every module the cluster requires |
| 500 | internal |
Unexpected error |
Some endpoints add their own codes, noted where they appear.
List endpoints return {"items": [...], "next_cursor": null}. All items come in one response; there are no limit or cursor parameters yet, and next_cursor is always null. Branches and bases are sorted by name, operations newest first.
Long-running operations
Section titled “Long-running operations”Creating a branch, resetting or deleting one, creating or snapshotting a base, loading a dump, refreshing a base and installing a runtime take from seconds to minutes. These calls return 202 Accepted at once, with a Location header and the operation:
{ "id": "op_01JC8Z6Q4M7V3X2N9K5R8T1W0B", "kind": "branch.create", "target": { "resource": "branch", "name": "feature-x" }, "node": "node1", "state": "running", "phase": "cloning", "created_at": "2026-11-09T12:00:00Z", "updated_at": "2026-11-09T12:00:03Z", "seq": 1762689600000000000}state is running, succeeded or failed. While it runs, phase says where it is. When it fails, error holds the reason; finished_at is set once it ends. The resource itself (the branch, for example) is visible right away with a status, so you can also poll it.
Poll GET /v1/operations/{id} until state is no longer running, or follow the event stream. An operation keeps running if the client disconnects. The newest 200 finished operations are kept.
Quick changes, such as starting a base or extending a branch, finish within the request and return 200 or 204.
Events
Section titled “Events”GET /v1/events is a Server-Sent Events stream of changes to branches, bases, operations, runtimes, nodes, settings and modules, so a client does not have to poll.
curl -skN -H "Authorization: Bearer dbb_..." https://node1:7080/v1/eventsid: 18343event: branch.updateddata: {"name":"feature-x","status":"ready", ...}
id: 18344event: operation.updateddata: {"id":"op_...","state":"succeeded", ...}| Event | Data |
|---|---|
branch.updated, base.updated |
The same object GET returns (a base without the can field) |
branch.deleted, base.deleted |
{"name": "..."} |
operation.updated |
The operation |
runtime.updated, runtime.deleted |
A runtime object, or {"runtime": "..."} when deleted |
node.updated |
A node changed; read GET /v1/cluster |
setting.updated |
The settings scope that changed |
module.updated, update.updated |
A change in modules or in a cluster update; fetch the resource again |
Each event has an id. After a dropped connection, reconnect with Last-Event-ID: <last id> to receive what you missed. If that is too old to replay, the server sends event: resync first and the client should reload the lists. A comment line (: ping) arrives every 15 seconds. A client that reads too slowly is disconnected and should reconnect. There is no way to filter by event type yet.
Not implemented
Section titled “Not implemented”The design notes mention a few things the API does not do yet: Idempotency-Key headers, ETag and If-Match, cursor pagination, and cancelling an operation. Do not rely on them.
Example: create a branch and connect
Section titled “Example: create a branch and connect”export DBB=https://node1:7080/v1export AUTH="Authorization: Bearer $DBB_TOKEN"
# 1. Create the branch. The answer is the operation.OP=$(curl -sk -H "$AUTH" -H 'Content-Type: application/json' \ -d '{"name":"feature-x","base":"prod","ttl":"72h"}' \ $DBB/branches | jq -r .id)
# 2. Wait until it finishes.while :; do STATE=$(curl -sk -H "$AUTH" $DBB/operations/$OP | jq -r .state) [ "$STATE" != running ] && break sleep 1doneecho "operation: $STATE"
# 3. Get the connection URL (includes the password).curl -sk -H "$AUTH" $DBB/branches/feature-x/dsn# {"dsn":"postgres://feature-x:...@node1:41203/postgres"}
# 4. Delete it when done.curl -sk -X DELETE -H "$AUTH" $DBB/branches/feature-xA branches token is enough for all four steps.