API Conventions
Conventions shared by every endpoint in the gitvetrix.com REST API: hosts, authentication, scopes, pagination, the error envelope, and the canonical merge-request paths. Per-resource reference pages assume the behavior described here and link back to it rather than repeating it.
Hosts
| Purpose | Host |
|---|---|
| REST API | https://api.gitvetrix.com/api/v1 |
| Web UI, sign-in, OAuth2 consent | https://www.gitvetrix.com |
| Local development stack | https://api.gitvetrix.test/api/v1 |
All REST endpoints live under the /api/v1 prefix; the version is part of the
path. The OpenAPI servers block in ../openapi/ lists the same
hosts.
Authentication
Every request that touches non-public data carries a bearer credential in the
Authorization header:
Authorization: Bearer <token>
Three credential types are accepted. They are validated by separate middleware and never cross paths.
Session JWT (interactive)
Exchange username and password for a short-lived JSON Web Token:
POST /api/v1/auth/login
Content-Type: application/json
{ "username": "alice", "password": "..." }
A successful login returns the access token and the caller's profile, and sets
a vetrix_access_token cookie (and a refresh cookie) on the response:
{
"access_token": "<jwt>",
"user": { "id": "<uuid>", "username": "alice", "...": "..." }
}
Send the JWT as Authorization: Bearer <jwt>. Access tokens are short-lived
(15 minutes by default); the browser refreshes them through the refresh cookie.
GET /api/v1/auth/me returns the authenticated user.
Personal access token (PAT)
A personal access token is a long-lived credential a user creates for scripts
and CLI use. The raw token has the form vetrix_<random>; it is shown once at
creation and stored only as a hash. Send it exactly like a JWT:
Authorization: Bearer vetrix_xxxxxxxxxxxxxxxxxxxxxx
PATs are also accepted as the password field of HTTP Basic auth, which is how git over HTTP and the package registries consume them.
OAuth2 access token
Third-party applications obtain an access token through the OAuth2
authorization-code flow. OAuth2 access tokens have the form vetrix_oat_<random>
(refresh tokens are vetrix_ort_<random>). They are sent as bearer tokens, the
same as the other two types.
Scopes
Two scope catalogs gate access. Which one applies depends on how the request authenticated.
OAuth2 scopes (granted at consent, verb:resource form) — for OAuth2
access tokens. Enforcement is additive to the underlying permission check: the
caller must hold both the scope and the ACL permission.
| Scope | Grants |
|---|---|
read:user |
Read the caller's profile fields |
read:repo / write:repo / admin:repo |
Browse / push / administer repositories |
read:issue / write:issue |
Read / create issues, comments, labels |
read:pipeline / write:pipeline |
View / trigger CI pipelines |
read:package / write:package |
Pull / push registry artifacts |
read:audit |
Read the instance audit log (instance admins only) |
openid / profile / email |
OpenID Connect sign-in and profile claims |
mcp:read |
Use the Model Context Protocol endpoint |
Personal-access-token scopes (resource:verb form) — selected when a PAT is
created: repo:read, repo:write, repo:admin, issue:read, issue:write,
ci:read, ci:write, registry:read, registry:write, admin:users,
admin:system, mcp:read, mcp:write.
The string mcp:read appears in both catalogs and names the same user-facing
grant — connecting an AI assistant to repositories the caller can read over the
MCP endpoint — but each catalog is enforced independently.
Session JWTs carry no scope list: they speak for the full set of permissions the signed-in user holds and bypass scope enforcement.
Pagination
List endpoints use one of two conventions.
Page / per-page envelope
Most list endpoints — for example the repository issue list
(GET /api/v1/repos/{owner}/{repo}/issues) — take page and per_page
query parameters and return a fixed envelope:
| Parameter | Default | Bounds |
|---|---|---|
page |
1 |
1-based; values below 1 fall back to 1 |
per_page |
25 |
A value of 0 or below falls back to the default; the upper bound varies by endpoint (see below) |
{
"items": [ ... ],
"total": 137,
"page": 1,
"per_page": 25
}
total is the unpaginated row count, so a client can compute the last page as
ceil(total / per_page).
Out-of-range handling is not uniform across endpoints. The issue list above
enforces no upper bound: a per_page of 0 or below falls back to the
default, but any positive value is honored, so per_page=200 returns up to
200 items. The common pagination helper — used by the pipeline list and other
resource lists — instead treats any per_page outside [1, 100] (zero,
negative, or above 100) as the default rather than clamping it, so for those
lists per_page=200 returns 25 items per page, not 100. Some admin and ACL
lists default to 50 or 100, accept a larger maximum, and clamp an
out-of-range value to that maximum. Check the per-resource page when the exact
bound matters.
A handful of endpoints accept the same page and per_page parameters but
return a bare JSON array instead of the envelope. The repository pipeline list
(GET /api/v1/repos/{owner}/{repo}/pipelines) is one: the response body is the
array of pipeline objects directly, with no enclosing items/total object,
so a client requests the next page when it receives a full page. Read the
per-resource page to confirm which shape an endpoint returns.
Limit / offset
A few endpoints instead take limit and offset (zero-based) directly and
report the count under total. The admin scope-grant list,
GET /api/v1/admin/admin-scopes?scope=<scope>, is one: limit defaults to 50
and is clamped to 500, offset defaults to 0, and the response is
{ "grants": [ ... ], "total": <int>, "limit": <int>, "offset": <int> }. The
OpenAPI LimitQuery / OffsetQuery parameters describe this form.
Errors
Every error response is JSON with a top-level error string:
{ "error": "repository not found" }
Some responses add an optional detail string, and validation failures may add
a stable machine-readable error_code. Branch client logic on the HTTP status
and error_code rather than on the human-readable error copy. See
errors.md for the full envelope and the status conventions
(including why a private resource returns 404 rather than 403).
Rate limits
Requests are metered by a Redis-backed limiter. A throttled request receives
429 Too Many Requests with X-RateLimit-* and Retry-After headers. See
rate-limits.md for the scope buckets, default limits, and
the full header set.
Merge requests: /merges is canonical
Merge-request endpoints live under:
/api/v1/repos/{owner}/{repo}/merges/...
The older /pulls/... paths are deprecated. Each /pulls/... route answers
with an HTTP 308 Permanent Redirect to the equivalent /merges/... path; the
308 preserves the request method, so a redirected POST stays a POST. New
integrations should call /merges directly.
Three wire-level identifiers intentionally keep the pull name for backward
compatibility and are not renamed: the merge_requests data is exposed
through the pull_request webhook event, and the live-update WebSocket path is
/ws/repos/{owner}/{repo}/pulls/{number}.