.gitbay/wiki/API.org
221 lines · 9207 bytes
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 wherever the registry is reached: over SSH, on the
API, anywhere. token create makes a read token unless --scope full
is given; a read token runs only commands marked read-only. A full-scope
token can mint another, but a token or SSH key with a --ttl cannot run any
command that creates a credential — token create, keys add,
repo deploy-key add, repo runner add, web login, admin invite,
admin user create, email verify, admin email verify — since what
it made would outlive it. Give a token the narrowest scope and shortest
TTL that does its job, and revoke it when the job is over.
gitbay auth token create --name ci [--scope read|full] [--ttl 30d]
gitbay auth token list
gitbay auth token revoke ci [--created]
Tokens and keys record the token they were created through. token
revoke prints what the token created, at any depth; with --created
it revokes those too, and their SSH connections close. Without it they
stay and the link is dropped.
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 are refused
by name; the token commands are not — a full-scope token can mint,
list and revoke tokens the same way it can run anything else.
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 ext/build --state pending
gitbay status set <owner/name> <sha> --context ext/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, each with updated_at; a ci/<job> status also carries
duration, read from the build behind it, once that build has
finished. With repo settings require-checks <repo> on, every status
on the MR head must be green, and a head something was going to report
on must carry some: a .gitbay/ci.yml with a job a push runs, or a
repository that has recorded a status before, which is what reporting
from outside through status set looks like. A repository where
nothing has ever reported merges. Each
report also emits a status event to webhooks.
Contexts starting with ci/ are the instance's own: its builds queue,
reuse, skip and finish them, and status set refuses them with exit 4,
so a writer cannot mark ci/test green on a head the build has not
passed. Report under another prefix, such as ext/.
repo settings require-contexts <repo> ext/deploy ci/test names
statuses the checks gate waits for whether or not they have reported:
one that has not is pending, and mr show lists it as
ext/deploy=missing. Naming any context turns require-checks on;
with no contexts the command clears the list and leaves
require-checks as it was. require-checks off keeps the list, which
waits for nothing until the gate is on again. repo settings show
prints both.
Webhooks
Per-repository outbound POSTs for repository events. Managed by repo admins:
printf %s "$SECRET" | gitbay webhook add <url> --secret - [--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>
The signing secret is read from stdin with --secret -; a value on the
command line is refused, since argv shows in process listings and shell
history. Over the JSON API it goes in the request's stdin field.
Events
Every event this forge records, and so every name --events may take.
The server holds the same list as control.EventKinds, and a test
fails if the code emits something not listed or lists something it never
emits — so this is the whole set, not a sample. webhook add refuses a
name that is not one of them, since a subscription to a typo would
silently never fire.
- repository:
push(ref, old, new, forced, deleted),repo.archived,repo.unarchived,repo.imported(from) - issues:
issue.created,issue.edited,issue.commented,issue.closed,issue.open,issue.labeled(labels),issue.assigned(assignees),issue.milestoned(milestone) - merge requests:
mr.created,mr.edited,mr.commented,mr.reviewed(verdict),mr.draft(draft),mr.retargeted(from, to),mr.labeled(labels),mr.milestoned(milestone),mr.merged(number, sha),mr.closed - releases:
release.created(tag),release.deleted(tag) - CI:
status,build.success,build.failure,build.cancelled
Every payload carries number where it names an issue or merge request.
There is deliberately no repo.deleted. Both events.repo_id and
webhooks.repo_id cascade from repos, so recording one would delete
it — and every webhook that could have received it — in the same
statement. A repository's deletion is visible in the audit log.
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, shared (100.64.0.0/10), link-local or multicast
addresses are refused both at registration and again at connect time,
unless the instance sets [webhooks] allow_local.