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.
-
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.
-
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.
-
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.
-
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.
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 |
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 |
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 |
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 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 |
conclusionissuccess,failure(it ran and found something) orerror(it could not run). Onlysuccessreleases the hold.- Long jobs can report twice. Post
"status": "in_progress"with no conclusion when the job starts, thenPATCH /api/v1/check-runs/{runId}with the conclusion when it ends. While a newer run is in progress, an older pass does not count. headSha256says what you tested. ReadcomparisonSourceSha256from 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_attachedtells 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.
| 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.