# Git API

18 operations under /api/v1. Errors are RFC 9457 problem+json with a stable `code` field. The complete schemas are in https://nebulactrl.com/api/v1/openapi.json.

## GET /git/connections

List git connections

Connections to GitHub, GitLab, Gitea, Forgejo and Bitbucket. Credentials and secrets are never returned.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Response 200: OK (ListConnectionsOutputBody)
  - `items` (ConnectionView[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 500 Internal Server Error

## POST /git/connections

Connect GitLab, Gitea, Forgejo or Bitbucket

Token connections are verified against the provider first (422 `credentials_invalid`). OAuth connections start `pending` and return an `authorizeUrl` to open in the browser; register `oauth.redirectUri` as the OAuth application's redirect URI. The response carries the webhook secret once; GitLab, Gitea and Forgejo webhooks are registered automatically when a service is linked, a Bitbucket webhook is created by hand (URL `webhookUrl`, this secret). Requires the `admin` scope.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Request body (application/json, required): CreateConnectionInputBody
  - `authType` (enum: token | oauth): token: a personal/group access token (needs api scope on GitLab; repository, webhook and status access on Gitea and Forgejo). oauth: an OAuth application. Ignored for Bitbucket.
  - `baseUrl` (string): The instance's web URL. Required for Gitea and Forgejo; defaults to https://gitlab.com and https://bitbucket.org
  - `clientId` (string): OAuth application client id
  - `clientSecret` (string): Write-only OAuth application client secret
  - `name` (string): Defaults to the provider and host
  - `provider` (enum: gitlab | gitea | forgejo | bitbucket, required): GitHub connects through a GitHub App: use POST /git/github/manifest
  - `token` (string): Write-only access token (authType token; for Bitbucket an optional repository access token to clone private repositories)

Response 201: Created (CreateConnectionResult)
  - `authorizeUrl` (string, required)
  - `connection` (ConnectionView, required)
  - `webhookSecret` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 409 Conflict; 422 Unprocessable Entity (`credentials_invalid`); 500 Internal Server Error

## GET /git/connections/{connectionId}

Get a git connection

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `connectionId` (path, string, required)

Response 200: OK (ConnectionView)
  - `accountLogin` (string, required)
  - `authType` (enum: github_app | oauth | token | webhook_only, required)
  - `baseUrl` (string, required)
  - `canListRepos` (boolean, required)
  - `createdAt` (string (date-time), required)
  - `github` (GitHubAppView, required)
  - `id` (string, required)
  - `manualWebhook` (boolean, required)
  - `name` (string, required)
  - `oauth` (OAuthView, required)
  - `provider` (enum: github | gitlab | gitea | forgejo | bitbucket, required)
  - `serviceCount` (integer (int64), required)
  - `status` (enum: pending | active | error, required)
  - `statusMessage` (string, required)
  - `supportsPreviews` (boolean, required)
  - `updatedAt` (string (date-time), required)
  - `webhookUrl` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## PATCH /git/connections/{connectionId}

Rename a git connection or replace its credentials

Requires the `admin` scope. Secrets are write-only.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `connectionId` (path, string, required)

Request body (application/json, required): UpdateConnectionInputBody
  - `clientSecret` (string): Write-only; replaces the OAuth client secret
  - `name` (string)
  - `token` (string): Write-only; replaces the access token

Response 200: OK (ConnectionView)
  - `accountLogin` (string, required)
  - `authType` (enum: github_app | oauth | token | webhook_only, required)
  - `baseUrl` (string, required)
  - `canListRepos` (boolean, required)
  - `createdAt` (string (date-time), required)
  - `github` (GitHubAppView, required)
  - `id` (string, required)
  - `manualWebhook` (boolean, required)
  - `name` (string, required)
  - `oauth` (OAuthView, required)
  - `provider` (enum: github | gitlab | gitea | forgejo | bitbucket, required)
  - `serviceCount` (integer (int64), required)
  - `status` (enum: pending | active | error, required)
  - `statusMessage` (string, required)
  - `supportsPreviews` (boolean, required)
  - `updatedAt` (string (date-time), required)
  - `webhookUrl` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict; 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /git/connections/{connectionId}

Remove a git connection

Refused with 409 `connection_in_use` while services use it, unless `force` is set (it unlinks them). Webhooks NebulaCtrl registered are removed; a GitHub App stays installed at GitHub. Requires the `admin` scope.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `connectionId` (path, string, required)
- `force` (query, boolean): Also unlink the services that use the connection

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`connection_in_use`); 422 Unprocessable Entity; 500 Internal Server Error

## GET /git/connections/{connectionId}/branches

List a repository's branches

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `connectionId` (path, string, required)
- `repo` (query, string, required): owner/name

Response 200: OK (BranchesOutputBody)
  - `items` (string[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error; 502 Bad Gateway

## GET /git/connections/{connectionId}/deliveries

Webhook delivery log of a connection

The last 200 deliveries (14 days), newest first: event, outcome (processed, ignored, rejected, replayed, error), what they started. Headers are redacted. Requires the `admin` scope.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `connectionId` (path, string, required)
- `cursor` (query, string): Opaque cursor from a previous page's nextCursor
- `limit` (query, integer (int64)): Page size (default 50, max 200)

Response 200: OK (PageWebhookDeliveryView)
  - `items` (WebhookDeliveryView[] | null, required)
  - `nextCursor` (string): Present when more items exist
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /git/connections/{connectionId}/oauth/start

Start (or restart) the OAuth authorization of a connection

Returns the provider URL to open in the browser.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `connectionId` (path, string, required)

Response 200: OK (AuthorizeOutputBody)
  - `authorizeUrl` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /git/connections/{connectionId}/refresh

Look up a GitHub App's installation again

For an App installed without returning to NebulaCtrl: finds the installation through the GitHub API and verifies the connection.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `connectionId` (path, string, required)

Response 200: OK (ConnectionView)
  - `accountLogin` (string, required)
  - `authType` (enum: github_app | oauth | token | webhook_only, required)
  - `baseUrl` (string, required)
  - `canListRepos` (boolean, required)
  - `createdAt` (string (date-time), required)
  - `github` (GitHubAppView, required)
  - `id` (string, required)
  - `manualWebhook` (boolean, required)
  - `name` (string, required)
  - `oauth` (OAuthView, required)
  - `provider` (enum: github | gitlab | gitea | forgejo | bitbucket, required)
  - `serviceCount` (integer (int64), required)
  - `status` (enum: pending | active | error, required)
  - `statusMessage` (string, required)
  - `supportsPreviews` (boolean, required)
  - `updatedAt` (string (date-time), required)
  - `webhookUrl` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## GET /git/connections/{connectionId}/repos

List the repositories a connection can access

The repository picker. 422 `no_repo_api` for Bitbucket (enter owner/name instead). Requires `projects:write`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `connectionId` (path, string, required)
- `q` (query, string): Filter by name
- `limit` (query, integer (int64))

Response 200: OK (ReposOutputBody)
  - `items` (Repo[] | null, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity (`no_repo_api`); 500 Internal Server Error; 502 Bad Gateway

## POST /git/connections/{connectionId}/test

Test a git connection

Calls the provider with the stored credentials and records the outcome in `status`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `connectionId` (path, string, required)

Response 200: OK (ConnectionView)
  - `accountLogin` (string, required)
  - `authType` (enum: github_app | oauth | token | webhook_only, required)
  - `baseUrl` (string, required)
  - `canListRepos` (boolean, required)
  - `createdAt` (string (date-time), required)
  - `github` (GitHubAppView, required)
  - `id` (string, required)
  - `manualWebhook` (boolean, required)
  - `name` (string, required)
  - `oauth` (OAuthView, required)
  - `provider` (enum: github | gitlab | gitea | forgejo | bitbucket, required)
  - `serviceCount` (integer (int64), required)
  - `status` (enum: pending | active | error, required)
  - `statusMessage` (string, required)
  - `supportsPreviews` (boolean, required)
  - `updatedAt` (string (date-time), required)
  - `webhookUrl` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /git/connections/{connectionId}/webhook-secret/rotate

Issue a new webhook secret (Bitbucket)

Only for providers whose webhook is created by hand (Bitbucket). The new secret is returned once; update the webhook at the provider.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `connectionId` (path, string, required)

Response 200: OK (SecretOutputBody)
  - `webhookSecret` (string, required): Shown once
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /git/github/manifest

Start creating a GitHub App

Creates a pending GitHub connection and returns a manifest: submit it as the form field `manifest` with a POST to `postUrl` (the browser does this; GitHub asks the user to confirm). GitHub then redirects back to NebulaCtrl, which stores the App and sends the user to install it. The App gets read access to code and metadata and write access to pull requests and commit statuses, and subscribes to push and pull_request events. Needs NEBULACTRL_PUBLIC_URL (422 `public_url_required`). Requires the `admin` scope.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Request body (application/json, required): ManifestInputBody
  - `baseUrl` (string): Web URL of a GitHub Enterprise Server (empty: github.com)
  - `name` (string): Connection name (default GitHub)
  - `owner` (string): GitHub organization that should own the App (empty: your own account)

Response 200: OK (ManifestStart)
  - `connectionId` (string, required)
  - `manifest` (string, required)
  - `postUrl` (string, required)
  - `state` (string, required)
Errors: 401 Unauthorized; 403 Forbidden; 422 Unprocessable Entity (`public_url_required`); 500 Internal Server Error

## GET /services/{serviceId}/ssh

Get a git service's SSH deploy key and pinned host keys

The public key and fingerprint, the provider's "add deploy key" link when the host is a known public instance, and each pinned host key (with the refused key when the host's key changed). The private key is never returned. 422 `not_git_source`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Response 200: OK (StatusView)
  - `deployKeyLink` (DeployKeyLink)
  - `host` (string)
  - `hostKeys` (HostView[] | null, required)
  - `key` (KeyView)
  - `usesSshUrl` (boolean, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity (`not_git_source`); 500 Internal Server Error

## POST /services/{serviceId}/ssh/host-keys/accept

Accept the SSH host key the git host presents now

Replaces the pinned host key of the service's git host after you reviewed the fingerprint of the refused key. 409 `host_key_changed` when the server presents yet another key, 502 `host_unreachable` when it cannot be reached. Needs `projects:write`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Request body (application/json, required): AcceptInputBody
  - `fingerprint` (string, required): The SHA256 fingerprint of the host key you reviewed; the key is pinned only if the server still presents it

Response 200: OK (StatusView)
  - `deployKeyLink` (DeployKeyLink)
  - `host` (string)
  - `hostKeys` (HostView[] | null, required)
  - `key` (KeyView)
  - `usesSshUrl` (boolean, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`host_key_changed`); 422 Unprocessable Entity; 500 Internal Server Error; 502 Bad Gateway (`host_unreachable`)

## POST /services/{serviceId}/ssh/key

Generate, upload or rotate the SSH deploy key

Without `privateKey` an Ed25519 key pair is generated; the public key goes on the repository as a deploy key. With `privateKey` your own unencrypted key is stored (write-only). An existing key is replaced only with `rotate: true`; otherwise 409 `ssh_key_exists`. Needs `projects:write`. Takes effect on the next deployment; nothing is staged.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Request body (application/json, required): SetKeyInputBody
  - `privateKey` (string): Write-only. An unencrypted OpenSSH or PEM private key to use instead of a generated one; omit to generate an Ed25519 key
  - `rotate` (boolean): Replace the existing key. Without it an existing key is a 409 ssh_key_exists

Response 200: OK (StatusView)
  - `deployKeyLink` (DeployKeyLink)
  - `host` (string)
  - `hostKeys` (HostView[] | null, required)
  - `key` (KeyView)
  - `usesSshUrl` (boolean, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 409 Conflict (`ssh_key_exists`); 422 Unprocessable Entity; 500 Internal Server Error

## DELETE /services/{serviceId}/ssh/key

Remove the SSH deploy key and the pinned host keys

Needs `projects:write`. A service whose git URL is an SSH URL cannot be deployed until a key is set again.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Response 204: No Content
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error

## POST /services/{serviceId}/ssh/test

Test the SSH connection to the repository

Fetches the configured branch over SSH with the deploy key. The first connection pins the host key; a changed key is refused and recorded. Always 200: `ok` and `code` (`no_key`, `not_ssh_url`, `host_key_changed`, `git_unreachable`) say what happened. Needs `projects:write`.

Authentication: session cookie or `Authorization: Bearer nct_…` token.

Parameters:
- `serviceId` (path, string, required)

Response 200: OK (TestResult)
  - `code` (string)
  - `commitSha` (string): The branch head, when the test succeeded
  - `message` (string)
  - `ok` (boolean, required)
  - `status` (StatusView, required)
Errors: 401 Unauthorized; 403 Forbidden; 404 Not Found; 422 Unprocessable Entity; 500 Internal Server Error
