# API: Gate

Decide what ships and set the policy behind it.

## Saved gate decisions

`GET /v1/decisions/{id}` accepts a saved release check UUID or gate decision UUID. With a check ID, it returns the latest recorded decision for that check; with a decision ID, it returns that exact record. Both the check and decision must belong to the authenticated key's workspace. Read-only workspace access is sufficient; no approval or write permission is granted by this endpoint.

The response contains `recorded`, `check` (the persisted check and retained findings), and `decision` when present. The decision includes `id`, `outcome`, `reasons` (including recorded CI policy reasons), `authorship`, retained `policy`, `pull_request`, `approvals`, `approvals_source`, `server_fetched`, `created_at` and `org_policy_version`. `server_fetched` is `true` when Bumpkit read the diff from your repository and `false` when your CI sent it; only `true` decisions count for deployments, votes, trains, the approvals inbox and agent track records. A legacy check without a gate decision returns `recorded: false` and a message; no result is inferred from its check verdict. Unknown and foreign workspace IDs return `404 DECISION_NOT_FOUND`.

It reads the saved decision and never re-runs the gate. A `null` policy text means that text wasn't kept.

`POST /v1/decisions/{check_id}/approve` and `POST /v1/decisions/{check_id}/deny` don't record votes. A vote is a person's call, so a key-authenticated request gets `403 HUMAN_APPROVAL_REQUIRED`. To vote from a terminal, `POST /v1/decisions/{check_id}/confirmations` with `{"vote": "approve"}` or `{"vote": "deny"}`. It needs workspace write permission and answers with `device_code`, `user_code`, `verification_url`, `expires_in` and `interval`. A person opens `verification_url` signed in to the dashboard and confirms; the vote is theirs. Poll `GET /v1/decisions/confirmations/{device_code}` with the same key: `202` while pending, then `200` with `result` (`check_id`, exact `head_sha`, `vote` and the latest gate `outcome`), `409 VOTE_REFUSED` when the confirmed vote was refused, `409 CONFIRMATION_DECLINED` when it was canceled, or `410 CONFIRMATION_EXPIRED`.

Confirmed votes follow the dashboard's rules: the voter needs a linked GitHub account that can review, the vote must be on the latest commit, and neither the pull request's author nor a coding agent's account can vote. The gate updates its decision after each vote. A second vote from the same person on the same commit replaces the first. Checks outside your workspace return `404`.

### Pending approvals

The release gate decisions waiting for a person's vote, as `bumpkit approvals list` shows them. Needs a key tied to a person. Reading never votes.

| Method | Route | Result |
| --- | --- | --- |
| GET | `/v1/approvals` | `workspace_id`, `workspace_name` and `requests`, up to 100: each has `id`, `check_id`, `repository`, `pull_request`, `head_sha`, `summary`, `reasons`, `votes` (`[{login, decision, source, at}]`), `link` to the check in the dashboard and `created_at` |
| GET | `/v1/approvals/{id}` | One request. `{id}` is the decision or check ID, or a prefix of at least 8 characters. `404 APPROVAL_NOT_FOUND` when nothing pending matches |

## Gate

The gate checks a release, applies the repository's `bumpkit.yml` policy and records a decision. See [Release Gate](/docs/release-gate) and [bumpkit.yml Reference](/docs/bumpkit-yml).

### POST /v1/gate

Takes every [`POST /v1/checks`](/docs/api-checks#post-v1-checks) field, plus:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `policy` | string | No | The repository's `bumpkit.yml` text. Empty means the default policy. When the GitHub App is installed on the repository for your workspace, the policy in your repository applies instead: see [Where the policy comes from](#where-the-policy-comes-from). |
| `approvals` | array | No | `[{login, source}]` approvals that count. When the GitHub App is installed for the repository, Bumpkit ignores these: with `pull_request` it reads the pull request's reviews instead, and without one no approvals count. |
| `pull_request` | integer | No | The pull request being gated |
| `hotfix` | boolean | No | Gate as a hotfix. See [Hotfixes & Break Glass](/docs/hotfixes). |
| `findings` | array | No | Findings computed on your side, such as blast radius (flag `blast_radius`) and missing backports. Bumpkit keeps only the kinds it accepts. |
| `comment` | boolean | No | Post or update the decision as a pull request comment through the GitHub App. It's marked as sent by a client and is separate from the App's own comment. |

Fields added by features in beta:

| Field | Type | Flag | Description |
|-------|------|------|-------------|
| `title`, `branch` | string | `jira`, `servicenow` | Used for change tickets and evidence |
| `specs` | array | `intent_check` | `[{source, ref, text}]` spec documents to check intent against, at most 20, 64 KB each |
| `agent_session` | string | `change_provenance` | The agent session that made the change. The gate outcome is recorded on the session only when the key that started it, or another key of the same person, runs the gate |
| `quality` | object | `architecture_drift`, `slop_tracker`, `performance_diff` | A quality snapshot from the CLI. See [Code Quality & Architecture](/docs/quality). |

`findings` also accepts quality kinds (`architecture_drift`, `code_quality`, `performance`, 50 per kind) when their flags are on, and `hotspot` (flag `behavior_checks`).


Returns `200` with a decision:

| Field | Description |
|-------|-------------|
| `id` | Decision ID |
| `outcome` | `pass`, `needs_approval` or `fail` |
| `reasons[]` | `rule`, `message`, `outcome`, `approvers` |
| `authorship` | `writer` (`human` or an agent), `tier` (`verified`, `declared`, `inferred` or `human`), `agents` and `committers` |
| `pull_request` | As sent |
| `check` | The full [check](/docs/api-checks#post-v1-checks) |
| `created_at` | When the decision was made |
| `commented` | `true` when the comment was posted |
| `replayed` | `true` on an [idempotent replay](/docs/api-auth#idempotency) (Beta) |
| `org_policy_version` | Org policy version applied (Beta, flag `org_policies`) |
| `stale_approvals` | Approvals that no longer count on GitLab, Bitbucket or Azure DevOps (Beta) |
| `intent` | The intent report, when `specs` were sent (Beta, flag `intent_check`) |
| `url` | The release page for this decision on the dashboard |
| `radar_url` | A link to the [blast radius radar](/docs/api-checks#get-v1-checks-id-radar-svg) on the release page, when the change reaches a product area (Beta, flag `blast_radar`) |
| `confidence` | The [confidence score](/docs/api-checks#get-v1-checks-id-confidence) (Beta, flag `confidence_score`) |
| `policy_source` | Where the policy came from: `client`, or `base_branch:<ref>` such as `base_branch:main` |
| `approve_in` | Where a person can approve: `bumpkit`, plus `slack` when Slack approvals are connected (Beta, flag `slack_approvals`) |
| `loop` | The [Close the loop](/docs/api-releases#close-the-loop) items for this branch and pull request (Beta, flag `close_the_loop`) |

Errors: `400 INVALID_POLICY` when `policy` doesn't parse (the gate fails closed), `400 VALIDATION_ERROR` when `source` is reserved (`github`, `gitlab`, `bitbucket`, `azure`, `azure_devops` and `dashboard` are for checks Bumpkit runs itself) or when `repository` can't resolve to one name (`www.github.com/acme/api`, `acme//api` and the repository's URL all resolve to `acme/api`; `acme/api/extra` and `acme/../api` are refused), `503 POLICY_UNAVAILABLE` (see below), plus the check errors. With org policies on, `503 ORG_POLICY_UNAVAILABLE` when the org policy can't be read or parsed.

#### Where the policy comes from

When the Bumpkit GitHub App is installed on the repository for your workspace, the gate reads `bumpkit.yml` itself and ignores the `policy` you send, including its `agents:` section:

| Request | Policy read from |
|---------|------------------|
| With `pull_request` into the default branch | The pull request's base commit |
| With `pull_request` into any other branch | The default branch |
| No `pull_request` | The default branch. `base` never picks the policy. |

`policy_source` names the ref, such as `base_branch:main`. When the policy you sent differs, the decision also carries a passing reason with the rule `policy_source: base_branch:main (policy read from your repository)`. If GitHub doesn't return the file, the gate fails closed with `503 POLICY_UNAVAILABLE`.

A decision from this endpoint uses the diff you sent, so with the App installed it's a report: it doesn't change the required check, a deployment, a vote or a release train.

Without the App, the gate uses the policy you send, `policy_source` is `client`, and the decision carries a passing reason with the rule `policy_source: client`, because Bumpkit can't prove a pull request didn't loosen its own rules.

### POST /v1/gate/override

Always `403 HUMAN_APPROVAL_REQUIRED` for an API key, whatever its owner's role. A break-glass override needs a person signed in to the dashboard: an owner, admin or policy admin, on the commit's check page. The message links to it.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `repository` | string | Yes | `owner/name` |
| `sha` | string | Yes | The full 40-character commit SHA |
| `reason` | string | Yes | 10 to 500 characters. Kept in the audit record. |
| `pull_request` | integer | No | Re-run the gate on this pull request |

The request shape is kept so the refusal is the same for every client.

### GET /v1/gate/overrides

Lists overrides. Query `limit` (1 to 500, default 100) and `overdue=true` (only overrides whose review is overdue).

Returns `{"overrides": [...]}` with `id`, `repository`, `sha`, `pull_request`, `reason`, `actor_email`, `created_at`, `review_due_at`, `reviewed_at`, `reviewed_by`, `review_note` and `status`.

### POST /v1/gate/overrides/{id}/review

Records the after-the-fact review of an override. Owners, admins, policy admins and approvers only (the `gate.review` permission). The person who overrode can't review their own override.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `note` | string | Yes | What the review found, 10 to 1000 characters |

Returns `204`. `404 NOT_FOUND` for an unknown override, `409 CONFLICT` if it was already reviewed.

### GET /v1/gate/connections

Lists the GitLab, Bitbucket and Azure DevOps repositories connected to the gate. Owner only. It answers even when those features are off, with whatever connections exist. Returns `{"connections": [{id, provider, host, path, webhook_url, created_at}]}`. Secrets are never listed.

### POST /v1/gate/connections

> **Beta, early access.**

Connects a repository host to the gate. Owner only.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `provider` | string | Yes | `gitlab`, `bitbucket`, `azure_devops` or `ghes` (GitHub Enterprise Server) |
| `host` | string | No | Public https API host. Defaults to `https://gitlab.com` for `gitlab` and `https://api.bitbucket.org/2.0` for `bitbucket`. Required for `azure_devops` and `ghes`. For `ghes`, give your server's address, like `https://github.acme.example`. |
| `path` | string | Yes | `group/project` (GitLab), `workspace/repository` (Bitbucket), `project/repository` (Azure DevOps) or `owner/repository`, or an owner alone (GitHub Enterprise Server) |
| `access_token` | string | Yes | Token Bumpkit uses to read the repository and report status. Up to 2048 characters. Stored encrypted. |

Returns `201` with the connection plus `webhook_secret`, which is shown only here. Point the host's webhook at `webhook_url` (see [Inbound webhooks](/docs/api-integrations#inbound-webhooks)). `409 CONFLICT` if the host and path are already connected.

### DELETE /v1/gate/connections/{id}

Needs a person signed in to the dashboard: an API key gets `403 HUMAN_APPROVAL_REQUIRED` with the link.

> **Beta.**

Removes a connection. Owner only. It works even when the provider's feature is off. Returns `204`, or `404 NOT_FOUND`.

### POST /v1/rules/compile

Compiles a plain-English rule into `bumpkit.yml` gate policy. Needs a key tied to a person. Limited to 20 compiles per 10 minutes per workspace.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `text` | string | Yes | The rule, up to 500 characters |
| `repository` | string | Yes | `owner/name` |
| `policy` | string | No | The current `bumpkit.yml`. Read from GitHub through the App when omitted. |
| `dry_run` | boolean | No | Replay the rule on recent gate decisions |
| `limit` | integer | No | Decisions to dry-run, 1 to 200, default 50 |

Returns `201` with a rule: `id`, `repository`, `text`, `summary`, `caveats`, `yaml`, `diff`, `policy_file`, `policy_source`, `policy_after`, `status`, `created_by`, `approved_by`, `approved_at`, `pull_request_url`, `dry_run` (`evaluated`, `skipped`, `changed[]`) and `created_at`.

Errors: `422 RULE_NOT_REPRESENTABLE` (the rule can't be expressed as policy), `503 LLM_NOT_CONFIGURED`, `429 RATE_LIMIT_EXCEEDED`.

### GET /v1/rules

Lists compiled rules. Query `limit`: 1 to 200, default 50. Returns `{"rules": [Rule]}`.

### GET /v1/rules/{id}

Returns one rule, or `404 NOT_FOUND`.

### POST /v1/rules/{id}/approve

Approves a rule. Owners, admins and policy admins only (the `policy.manage` permission). Send `{"open_pr": true}` to open a pull request with the policy change through the GitHub App. Returns `200` with the rule, `status` now `approved` or `pr_opened`. `409` if it is already approved.

## Org policies & freezes

> **Beta, early access.** Flags: `org_policies`, `change_freezes`. Until your workspace has access, these endpoints answer `403 FEATURE_EARLY_ACCESS`.

An org policy is a `bumpkit.yml` layer applied to every repository in the workspace. Freezes block or slow releases for a period. See [Org Policies & Freezes](/docs/org-policies). These endpoints need a key tied to a person, except `POST /v1/policy/effective`.

### GET /v1/policy/org

Returns the current org policy: `version`, `content`, `repository`, `message`, `actor_email`, `created_at`. `404 NOT_FOUND` if there is none.

### PUT /v1/policy/org

Needs a person signed in to the dashboard: an API key gets `403 HUMAN_APPROVAL_REQUIRED` with the link.

Sets the org policy. Owner only. Send exactly one of `content`, `repository` or `clear`.

| Field | Type | Description |
|-------|------|-------------|
| `content` | string | The policy YAML, up to 64 KB |
| `repository` | string | `owner/name` of a config repository. Bumpkit reads its `bumpkit.yml` from the default branch through the GitHub App. |
| `clear` | boolean | Remove the org policy |
| `message` | string | Why it changed, up to 500 characters |

Each change creates a new version. Returns `200 {"policy": OrgPolicy, "diff": "..."}`. `409 CONFLICT` if nothing changed, there is nothing to clear, or someone else changed it at the same time.

### GET /v1/policy/org/versions

Lists versions, newest first. Query `limit`: 1 to 200, default 50. Returns `{"versions": [OrgPolicy]}`.

### GET /v1/policy/org/versions/{version}

Returns one version. `400` if `version` isn't a positive number, `404 NOT_FOUND` if it doesn't exist.

### POST /v1/policy/effective

Shows the policy the gate applies: the org layer merged with a repository's policy. Works with any key.

| Field | Type | Description |
|-------|------|-------------|
| `policy` | string | The repository's `bumpkit.yml` text |

Returns `org_version`, `org_repository`, `policy` (merged YAML) and `sources` (`[{key, value, from, locked, note}]`, where each key's value came from). `400 INVALID_POLICY` if the merge fails, `503 ORG_POLICY_UNAVAILABLE` if the org policy can't be read or parsed.

### GET /v1/freezes

Lists active and upcoming freezes, plus those created in the last 30 days, up to 100. Returns `{"freezes": [Freeze]}`.

Each freeze has `id`, `repository` (empty means every repository), `reason`, `mode`, `hotfix`, `starts_at`, `ends_at`, `ended_at`, `ended_by`, `actor_email`, `active` and `created_at`.

### POST /v1/freezes

Declares a freeze. Owners, admins and policy admins only (the `policy.manage` permission). Other roles get `403 FORBIDDEN`, and the refusal is written to the audit log as `freeze.declare.denied`.

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `reason` | string | Yes | 10 to 500 characters |
| `repository` | string | No | `owner/name`. Omit to freeze every repository. |
| `mode` | string | No | `block` (default) or `approve` |
| `hotfix` | string | No | `allow`, `approve` (default) or `block` |
| `starts_at` | string | No | RFC 3339. Defaults to now. |
| `ends_at` | string | No | RFC 3339. Must be after `starts_at` and in the future. |

Returns `201` with the freeze.

### POST /v1/freezes/{id}/end

Needs a person signed in to the dashboard: an API key gets `403 HUMAN_APPROVAL_REQUIRED` with the link.

Ends a freeze. Owners, admins and policy admins only; other roles get `403 FORBIDDEN`, audited as `freeze.end.denied`. Returns `200` with the freeze. `404 NOT_FOUND`, or `409 CONFLICT` if it is already over.
