A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Wiki: API

API

gitbay API and webhooks

The JSON API

One endpoint fronts the entire command registry — everything the SSH control plane can do, current and future, with identical semantics. Disabled by default; the instance must set:

[api]
enabled = true

Tokens

Tokens are minted over SSH and only over SSH — an API token can never create further credentials.

gitbay auth token create --name ci [--scope full|read] [--ttl 30d]
gitbay auth token list
gitbay auth token revoke ci

The token (prefix gb_, shown exactly once) is presented as Authorization: Bearer gb_.... Only a hash is stored server-side. Scope read permits list/show/log/diff-style commands and refuses anything that modifies state. --ttl takes Go durations or a day suffix (30d); expired, revoked, and unknown tokens all answer the same 401.

POST /api/v1/cmd

Request body:

{"argv": ["issue", "create", "you/project", "--title", "from CI"],
 "stdin": "optional body for --file - style input"}

argv is real argv — no shell, no quoting rules. The response is the command's own JSON envelope with exit_code added, plus stderr when the command wrote diagnostics:

{"protocol_version": 1, "exit_code": 0, "data": {"number": 7}}

HTTP status maps the exit code: 0→200, 2→400, 3→404, 4→403, else 500. Commands that emit raw text rather than an envelope (help, mr diff) come wrapped as {"output": "..."}. Git transport commands and the token commands are refused by name.

curl -s -H "Authorization: Bearer $TOKEN" \
     -d '{"argv":["whoami"]}' https://gitbay.org/api/v1/cmd

printf '{"argv":["issue","create","you/project","--title","t","--file","-"],"stdin":"body\n"}' |
curl -s -H "Authorization: Bearer $TOKEN" -d @- https://gitbay.org/api/v1/cmd

GET /api/v1/read

The conditional half: the same registry over GET, admitting only commands the registry marks read-only, so a GET structurally cannot mutate. Responses carry an ETag; If-None-Match answers 304.

curl -s -H "Authorization: Bearer $TOKEN" \
     "https://gitbay.org/api/v1/read?argv=repo&argv=show&argv=you/project"

The dashboard read

dashboard returns the account aggregate — pinned repositories, open merge requests, assigned issues, recent builds — in one call:

curl -s -H "Authorization: Bearer $TOKEN" \
     "https://gitbay.org/api/v1/read?argv=dashboard"

For an instance admin the response also carries {"server": {"commit": "<sha>"}}, the build the daemon is running, so the deployed commit is readable without the journal. The key is absent for everyone else: the exact build a host runs narrows down which known issues apply to it.

Cursor pagination

issue list, mr list, repo list, and feed accept --limit <n> (1–200) and --cursor <c>. With either flag present the data envelope becomes {"items": [...], "next": "..."}; next is an opaque cursor for the following page, absent on the last one. Without the flags the response stays the complete bare array; a bare feed returns the newest 50 events.

Commit statuses (CI reporting)

CI reports results through the same command surface (over SSH or the JSON API with a full-scope token; reporting requires write access):

gitbay status set <owner/name> <sha> --context build --state pending
gitbay status set <owner/name> <sha> --context build --state success --url https://ci.example/run/1
gitbay status list <owner/name> <sha> --json    # {"combined": "...", "statuses": [...]}

One row per (commit, context): re-reporting updates in place. States: pending, success, failure, error; the combined state is the worst of them. Statuses appear on commit pages, MR pages, and mr show. With repo settings require-checks <repo> on, merging requires the MR head to carry statuses and all of them green. Each report also emits a status event to webhooks.

Webhooks

Per-repository outbound POSTs for repository events. Managed by repo admins:

gitbay webhook add <url> --secret s3cret [--events push,issue.created]  # default *
gitbay webhook list
gitbay webhook deliveries [--limit 50]     # status, attempts, last error
gitbay webhook redeliver <delivery-id>     # requeue, including dead letters
gitbay webhook remove <id>

Events

push (data: ref, old, new, forced, deleted), issue.created, issue.commented, issue.closed, issue.open, mr.created, mr.merged (data: number, sha), repo.imported (data: from).

Delivery

Each event POSTs one JSON body:

{"event": "push",
 "repo": "you/project",
 "actor": "alice",
 "created_at": "2026-08-24T01:00:00.000Z",
 "data": {"ref": "refs/heads/main", "old": "...", "new": "...",
          "forced": false, "deleted": false}}

Headers: X-Gitbay-Event, X-Gitbay-Delivery (id), and — when the hook has a secret — X-Gitbay-Signature-256: sha256=<hex hmac> over the raw body. Verify before trusting:

import hmac, hashlib
expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers["X-Gitbay-Signature-256"])

A 2xx within 10 seconds is success. Anything else retries with exponential backoff (30s base, doubling) and dead-letters after five attempts; webhook deliveries shows the trail and redeliver revives a dead letter. Redirects are never followed, and targets resolving to loopback/private/link-local addresses are refused both at registration and again at connect time, unless the instance sets [webhooks] allow_local.