# The HTTP API

> BuildGate has an HTTP API for your own systems: read the deploy requests, raise one, work the exception queue, and pull the deploy records an auditor is shown. An administrator creates a token in the console. No token can approve a deploy.

Source: https://docs.buildgate.dev/api
Documentation: https://docs.buildgate.dev

## What it is for, and what it will not do

Everything else that reaches BuildGate does so as somebody who is present: the console and the extension with a session, an [MCP client](https://docs.buildgate.dev/mcp) with a token an approver authorized after signing in. That is the right shape for a person and the wrong shape for a nightly job. This is the other door — a credential an administrator creates, scoped to what it needs, and revocable on its own.

Three things it is good for, in rough order of how often teams ask:

- **Evidence, on a schedule.** Pull every production change with who requested it, who approved it and when it shipped, into a warehouse or a GRC platform, instead of somebody downloading a CSV once a quarter.
- **Raising deploys from your own pipeline.** A request raised over HTTP is the same record as one raised in the Bubble editor, routed to the same colleague, with the same Linear issue behind it.
- **Watching the queue.** Follow a request to its decision without holding a connection open, and without re-reading it in a loop.

> **No token can approve a deploy.** There is no scope that grants it, no endpoint that would take it, and the database refuses a token that names it. An approval is evidence that a named person who was not the requester agreed, and possession of a string is not that — so approving lives in the console and in an MCP client somebody authorized, and nowhere else. This is the same argument behind [there being no approve button in Slack](https://docs.buildgate.dev/mcp#identity).

**Nothing here deploys, either.** There is no endpoint that releases a deploy token or records a completion. The deploy still runs from the Bubble editor with the extension, once somebody else has approved.

## Creating a token

In the console, under **Settings → API**. Owners and admins only — the list of credentials that can read an organization's deploy records is the list of doors into it, and it belongs to the people who can change it.

1. **Name it for what it does.**

   "Warehouse sync", not "token 3". The name is on the settings screen and in the audit chain, so that "which of these four is the warehouse" has an answer a year from now.
2. **Turn on only the scopes it needs.**

   Each one says what it lets the holder do to production, in those terms. See [the five scopes](https://docs.buildgate.dev/api#scopes).
3. **Choose when it expires.**

   Thirty days, ninety, a year, or never. Never is a real answer for a machine credential and the console makes you pick it rather than defaulting you into it.
4. **Copy it. It is shown once.**

   BuildGate keeps only a hash, so there is no screen that could show it again and no support request that could recover it. Lose it and you revoke that one and create another.

**A token acts as you.** It is not a service account with an identity of its own: every call it makes is made as the administrator who created it, under their role, and that is the name the record keeps. Which means two things worth knowing before you create one.

- **It is never more powerful than you are today.** Your role is read out of the database on every call, not copied onto the token. Made a deployer next month, and the token stops resolving exceptions that afternoon.
- **It dies with your membership.** Remove somebody from the organization and their tokens stop working on their very next call. There is nothing to sweep and nobody to remember — which also means a token belonging to whoever is about to leave is a job that will do itself, loudly, on a Tuesday. Create these under an account that is staying.

Revoking is immediate and cannot be undone. The row stays on the list, marked withdrawn, because "what could reach us in March" is a question asked in June.

## Making a call

Every endpoint lives under `/api/v1` on this deployment's own address, and every call carries the token as a bearer credential:

`curl -H "Authorization: Bearer bg_…" https://buildgate.dev/api/v1/whoami`

Start with `whoami`. It answers with the organization, the person the token acts as, their role and the scopes it holds — and it is the only endpoint that needs no scope, because a token that cannot find out what it is cannot do anything sensibly.

**One token, one organization.** The organization is not a parameter anywhere in this API: it is a property of the token, so a call has nowhere to name another one. If you administer two, create a token in each. An id from the other organization reads exactly as an id belonging to nobody — a 404, not a 403, because a refusal that confirmed the record existed elsewhere would answer a question nobody was entitled to ask.

Requests and responses are JSON. Times are ISO-8601 strings in UTC. Ids are the same ids the console shows.

## The five scopes

A token holds the ones you switch on and nothing else. They are the same scopes an MCP client asks an approver for, described the same way, minus the one that decides.

| Scope | What granting it means |
| --- | --- |
| `approvals:read` | Read the deploy requests in this organization, and the change each one describes |
| `approvals:create` | Raise a deploy request as you, naming the app, the issue and who is being asked. Nothing is deployed by this |
| `exceptions:read` | Read the compliance exceptions — deploys that went around the gate, break-glass uses, and how each was handled |
| `exceptions:resolve` | Close those exceptions as you, with a note. Nothing is deleted |
| `evidence:read` | Read the deploy records an auditor is shown, and verify the audit chain they come out of |
| `checks:write` | Post check results onto deploy requests, as your CI would. A result can hold an approval or release that hold; it never approves anything |

**A scope is a ceiling, never a grant.** Switching on `exceptions:resolve` does not make anybody an approver: the call still runs through the same check the console does, which reads the role of the person the token acts as. A token created by a deployer with that scope resolves exactly nothing, and is refused with `forbidden` rather than with a scope error — because the scope was not the problem.

`approvals:decide` exists in BuildGate and is deliberately not on this list. See [above](https://docs.buildgate.dev/api#what-it-is).

## The endpoints

| Endpoint | Scope | What it does |
| --- | --- | --- |
| `GET /api/v1/whoami` | any | The organization, the person this token acts as, their role, its scopes and its expiry |
| `GET /api/v1/apps` | `approvals:read` | The Bubble apps in this organization, with the branches each deploy moves between |
| `GET /api/v1/deploy-requests` | `approvals:read` | Requests, most recent first. `status` filters (`pending`, `approved`, `deployed`, `rejected`, `withdrawn`, `expired`, `failed`, or `all`); `limit` up to 100 |
| `GET /api/v1/deploy-requests/updates` | `approvals:read` | What has happened since a `cursor`. See [following a request](https://docs.buildgate.dev/api#following) |
| `GET /api/v1/deploy-requests/{id}` | `approvals:read` | One request in full: the change, the rollback plan, the issue, the decision if there is one |
| `GET /api/v1/approvers` | `approvals:create` | Who could approve a request this token raises. Never the person it acts as |
| `POST /api/v1/deploy-requests` | `approvals:create` | Raise one. See [raising a deploy request](https://docs.buildgate.dev/api#raising) |
| `GET /api/v1/exceptions` | `exceptions:read` | The exception queue. `status` (`open`, `resolved`, `all`), `type` (comma-separated), `bubbleAppId` |
| `POST /api/v1/exceptions/{id}/resolve` | `exceptions:resolve` | Close one open finding. Body: `resolutionNote`. It deletes nothing |
| `GET /api/v1/deployments` | `evidence:read` | One row per production change in a window. `from` and `to`, defaulting to the last 90 days |
| `GET /api/v1/chain/verify` | `evidence:read` | Re-computes every hash in this organization's chain and checks the anchors |
| `POST /api/v1/deploy-requests/{id}/check-runs` | `checks:write` | Post a check result onto a request still waiting for a decision. See [posting check results](https://docs.buildgate.dev/api#posting-checks) |
| `PATCH /api/v1/check-runs/{runId}` | `checks:write` | Finish a run posted as `in_progress`. Only the token that posted it can, and only once |
| `GET /api/v1/deploy-requests/{id}/check-runs` | `approvals:read` | Every run of every check on a request, BuildGate's own and posted ones, newest first |

The two evidence endpoints additionally need the person the token acts as to hold an approving role, exactly as the console's own exports do.

## Raising a deploy request

`POST /api/v1/deploy-requests`, with six fields: `bubbleAppId`, `approverEmail`, `linearIssueKey`, `summary`, `changeDescription` and `rollbackNote` — and optionally `issueProvider`, `linear` or `clickup`. `linearIssueKey` keeps its name for callers written before ClickUp and takes a ClickUp task id or custom id just as well; without `issueProvider` every connected tracker is asked, Linear first. A deploy that ships more than one issue names the rest in `additionalIssueKeys`, an array of keys looked up the same way, up to ten issues in all; the request answers with every one of them in `issues`. To ask more than one person at once, name the rest in `additionalApproverEmails`, up to ten approvers in all: each is notified and any of them may approve, and the app's rulesets still decide how many approvals complete it. It answers `201` with the request, whose `approvalUrl` is the page the approver signs in to and decides on — hand that to them.

It is the same record as one raised from the Bubble editor, and it is held to the same rules. Your membership and the named approver's role are checked, you still cannot name yourself, the approver is notified as usual, and the issue is looked up in your own Linear or ClickUp by its key rather than taken as written — there is no field here for a title, because the link is evidence and has to come from the tracker.

Two things are worth care, because a program will happily get both wrong.

- **The change description and the rollback plan are the evidence.** A person reads them to decide, and an auditor reads them afterwards. "Various fixes" is not evidence — say what changed, what was checked, and how it gets undone.
- **There is no diff behind it yet.** A request raised this way carries no branch comparison until the extension exports the two branches, so the approver may be reading your description and nothing else.

The record says it came through the API, and names the token. A person at a keyboard, a person's agent and a program running on a schedule are three different acts, and the timeline says which.

## Following a request to its decision

**Nothing here pushes.** This service holds no connection open and no subscription, so a program follows a request by asking what has changed since the last time it asked. If you want events delivered instead, that is what [webhooks](https://docs.buildgate.dev/integrations) are for.

Call `GET /api/v1/deploy-requests/updates` once with no cursor to learn where the record is now; it answers with a `cursor` and no updates, because a subscription starts when you subscribe. Call it again with that cursor and it answers with everything that has happened since — approved, rejected, deployed, expired, withdrawn, commented on, reviewed — and a new one. Pass `deployRequestId` to follow one request rather than everything you can see.

Nothing between two cursors is ever skipped: the cursor is a position in the audit chain, and the chain has no gaps. So this is how to wait for a decision, rather than re-reading the request in a loop — and there is no faster answer to be had, because a decision cannot arrive between two polls in a way that leaves no event behind. The response carries `pollAfterSeconds`, which is a suggestion rather than a limit.

## Posting check results

Your own CI can be a required check. A ruleset lists it by name under *Require checks to pass → External checks*, and from then on a request for a targeted app cannot be approved until a passing result has been posted under that name. It works the way a status check works on a pull request: your job runs, then reports what it found.

`curl -X POST -H "Authorization: Bearer bg_…" -H "Content-Type: application/json" -d '{"name":"ci/tests","conclusion":"success","summary":"212 tests passed"}' https://buildgate.dev/api/v1/deploy-requests/{id}/check-runs`

| Field | What it is |
| --- | --- |
| `name` | The name a ruleset requires, as your CI names the job: `ci/tests`, `e2e (checkout)` |
| `status` | `completed`, the default, or `in_progress` for a job that has only started |
| `conclusion` | For a completed run: `success`, `failure` or `error` |
| `summary` | One line, up to 200 characters: `212 tests passed` |
| `detail` | Optional. What failed, in up to 4,000 characters; the approver reads it |
| `detailsUrl` | Optional. An `https://` link to the run in your CI |
| `headSha256` | Optional. The request's `comparisonSourceSha256`, saying which snapshot you tested |

- **`conclusion`** is `success`, `failure` (it ran and found something) or `error` (it could not run). Only `success` releases the hold.
- **Long jobs can report twice.** Post `"status": "in_progress"` with no conclusion when the job starts, then `PATCH /api/v1/check-runs/{runId}` with the conclusion when it ends. While a newer run is in progress, an older pass does not count.
- **`headSha256` says what you tested.** Read `comparisonSourceSha256` from the request and send it back. A pass about a different snapshot is recorded, and never releases the hold. Leave it out if your job does not test the comparison's snapshot.
- **When to start.** A [webhook](https://docs.buildgate.dev/integrations#webhooks) subscribed to `comparison_attached` tells your CI the moment a request has a snapshot to test, and carries its hash.
- **It never approves.** A pass releases one condition an approval waits on. Somebody other than the requester still has to approve, and the result is recorded against the person your token acts as, with the token's name beside it.

Results can only be posted onto a request still waiting for a decision, and are never changed afterwards. A result posted under a name no ruleset requires is recorded and listed with the request's runs, and holds nothing.

## Pulling the evidence

`GET /api/v1/deployments` answers with the same records behind the console's auditor CSV and behind what BuildGate pushes to Vanta — one row per production change, from one query, so three artifacts cannot disagree about who approved what.

Each row names the app and the branches, the linked issue, who requested it, who approved it, and — where the approval was not a person — which automated reviewer cleared it and on what evidence. Read [Evidence and audit](https://docs.buildgate.dev/evidence) before you build a report on these fields: several of them can only assert something narrower than they look like they assert, and that page is where the limits are written down.

The CSV itself is not on this API. An auditor's artifact is something a person downloads and opens, and a program reading a feed should not have to parse a spreadsheet — so the export stays in the console and this returns JSON.

`GET /api/v1/chain/verify` re-computes every hash in the chain and checks the anchors. It answers `200` when the chain verifies and `409` when it does not — and that `409` body is the finding, naming the event that broke and why. Treat it as an answer, not as a failed request. Running it on a schedule is a reasonable thing to do; it is the check an auditor would otherwise have to take on trust.

## When a call is refused

Refusals carry the service's own vocabulary, in the shape `{"error": {"code": "…", "message": "…"}}`, so a client is told what the console would be told rather than that something went wrong.

| Status | Code | What to do |
| --- | --- | --- |
| 401 | `unauthenticated` | No token was sent. The `WWW-Authenticate` header says so without naming an error, because nothing was presented to call invalid |
| 401 | `invalid_token` | Revoked, expired, not a BuildGate API token, or the person it acts as has left the organization or been deactivated. The header says which |
| 403 | `insufficient_scope` | The token was not given that scope. The header names the one it needed; create a token that has it |
| 403 | `forbidden` | The scope was there and the *person* may not do it. Their role changed, or the token was created by somebody who never could |
| 404 | `not_found` | No such record in this organization. An id from another one reads the same way, deliberately |
| 400 | `validation_failed` | The message names the field |
| 409 | `self_approval_forbidden` | You named yourself as the approver. That rule has no exceptions |
| 429 | `rate_limited` | Counted per token, not per address. `Retry-After` says how long |

The limit is set well above honest use — a job pulling records nightly and an agent following a request will not meet it. A tight polling loop will, and the fix for that is the cursor rather than a higher limit.

## What may change under you

The path says `v1` from the first release rather than from the first breaking change, which is the only time it is cheap to add.

Inside `v1`, expect fields to be *added* to responses and new endpoints to appear. Parse permissively: a client that rejects an object because it carries a key it has not seen will break on a release that took nothing away. Fields will not be removed or change meaning under `v1`; that is what a `v2` would be for.

One thing will not change whatever else does. No version of this API will approve a deploy.
