BuildGate

For administrators and engineers

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.

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 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.

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.

  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.

ScopeWhat granting it means
approvals:readRead the deploy requests in this organization, and the change each one describes
approvals:createRaise a deploy request as you, naming the app, the issue and who is being asked. Nothing is deployed by this
exceptions:readRead the compliance exceptions — deploys that went around the gate, break-glass uses, and how each was handled
exceptions:resolveClose those exceptions as you, with a note. Nothing is deleted
evidence:readRead the deploy records an auditor is shown, and verify the audit chain they come out of
checks:writePost 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.

The endpoints

EndpointScopeWhat it does
GET /api/v1/whoamianyThe organization, the person this token acts as, their role, its scopes and its expiry
GET /api/v1/appsapprovals:readThe Bubble apps in this organization, with the branches each deploy moves between
GET /api/v1/deploy-requestsapprovals:readRequests, most recent first. status filters (pending, approved, deployed, rejected, withdrawn, expired, failed, or all); limit up to 100
GET /api/v1/deploy-requests/updatesapprovals:readWhat has happened since a cursor. See following a request
GET /api/v1/deploy-requests/{id}approvals:readOne request in full: the change, the rollback plan, the issue, the decision if there is one
GET /api/v1/approversapprovals:createWho could approve a request this token raises. Never the person it acts as
POST /api/v1/deploy-requestsapprovals:createRaise one. See raising a deploy request
GET /api/v1/exceptionsexceptions:readThe exception queue. status (open, resolved, all), type (comma-separated), bubbleAppId
POST /api/v1/exceptions/{id}/resolveexceptions:resolveClose one open finding. Body: resolutionNote. It deletes nothing
GET /api/v1/deploymentsevidence:readOne row per production change in a window. from and to, defaulting to the last 90 days
GET /api/v1/chain/verifyevidence:readRe-computes every hash in this organization's chain and checks the anchors
POST /api/v1/deploy-requests/{id}/check-runschecks:writePost a check result onto a request still waiting for a decision. See posting check results
PATCH /api/v1/check-runs/{runId}checks:writeFinish a run posted as in_progress. Only the token that posted it can, and only once
GET /api/v1/deploy-requests/{id}/check-runsapprovals:readEvery 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 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

FieldWhat it is
nameThe name a ruleset requires, as your CI names the job: ci/tests, e2e (checkout)
statuscompleted, the default, or in_progress for a job that has only started
conclusionFor a completed run: success, failure or error
summaryOne line, up to 200 characters: 212 tests passed
detailOptional. What failed, in up to 4,000 characters; the approver reads it
detailsUrlOptional. An https:// link to the run in your CI
headSha256Optional. 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 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 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.

StatusCodeWhat to do
401unauthenticatedNo token was sent. The WWW-Authenticate header says so without naming an error, because nothing was presented to call invalid
401invalid_tokenRevoked, expired, not a BuildGate API token, or the person it acts as has left the organization or been deactivated. The header says which
403insufficient_scopeThe token was not given that scope. The header names the one it needed; create a token that has it
403forbiddenThe scope was there and the person may not do it. Their role changed, or the token was created by somebody who never could
404not_foundNo such record in this organization. An id from another one reads the same way, deliberately
400validation_failedThe message names the field
409self_approval_forbiddenYou named yourself as the approver. That rule has no exceptions
429rate_limitedCounted 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.