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