API Reference
API Reference
Section titled “API Reference”Status: Authoritative API reference.
Cross-references: ARCHITECTURE.md (system overview), SECURITY.md (authentication model), UX DESIGN (dashboard/admin contract).
Authentication
Section titled “Authentication”The API supports three authentication models:
Bearer token (dashboard): Supabase session access token via Authorization: Bearer header. Used by the dashboard UI.
Authorization: Bearer <supabase-session-token>API key (admin/internal): Hash-based tenant API key via X-API-Key header. Used by internal service-to-service calls.
X-API-Key: <tenant-api-key>Admin key: The X-Admin-Key header grants full access to admin endpoints without a tenant_id context. Set via the ADMIN_KEY secret.
X-Admin-Key: <admin-key>Error format
Section titled “Error format”All error responses use a consistent envelope:
{ "code": "invalid_key", "message": "Invalid API key"}Error codes
Section titled “Error codes”| Code | HTTP Status | Description |
|---|---|---|
unauthorized |
401 | No Bearer token or X-API-Key provided |
missing_auth |
401 | Bearer token or X-API-Key required |
invalid_key |
401 | API key does not match any tenant |
invalid_token |
401 | Invalid or expired Supabase session token |
not_found |
404 | Resource not found |
invalid_body |
400 | Malformed request body |
db_error |
500 | Database error |
repo_already_managed |
409 | Repo already managed by another tenant on the same instance |
failed_to_create_connection |
500 | Connection creation failed |
decryption_error |
500 | Could not decrypt credential |
forge_unreachable |
502 | Could not reach the forge instance |
rate_limited |
429 | Too many requests |
Dashboard endpoints (Bearer auth)
Section titled “Dashboard endpoints (Bearer auth)”These endpoints require a valid Supabase session token.
GET /api/v1/connections
Section titled “GET /api/v1/connections”List the authenticated user’s forge connections. Token fields are never returned.
GET /api/v1/connectionsAuthorization: Bearer <token>Response (200):
[ { "id": "660e8400-...", "tenant_id": "550e8400-...", "instance_url": "https://git.example.com", "bot_username": "mq-bot", "token_type": "pat", "created_at": "2026-08-04T12:00:00Z" }]POST /api/v1/connections
Section titled “POST /api/v1/connections”Create a new forge connection. Token fields are never returned.
Dedup: If a connection with the same (tenant_id, instance_url, bot_username) already exists, the endpoint returns the existing connection with existing: true and HTTP 200 (not 201).
POST /api/v1/connectionsAuthorization: Bearer <token>Content-Type: application/json
{ "instance_url": "https://git.example.com", "token": "ghp_xxxxxxxxxxxx", "bot_login": "mq-bot", "forge_type": "forgejo"}Response (201 — new):
{ "id": "660e8400-...", "tenant_id": "550e8400-...", "instance_url": "https://git.example.com", "bot_username": "mq-bot", "token_type": "pat", "provider": "forgejo", "created_at": "2026-08-04T12:00:00Z"}Response (200 — duplicate):
{ "existing": true, "id": "660e8400-...", "provider": "forgejo"}PATCH /api/v1/connections/:id
Section titled “PATCH /api/v1/connections/:id”Rotate the connection’s token and/or update the bot username. The new token is encrypted at rest. Returns the connection without the token field.
PATCH /api/v1/connections/:idAuthorization: Bearer <token>Content-Type: application/json
{ "token": "ghp_new_token_xxx", // optional; omit to keep current "bot_login": "mq-bot" // optional; omit to keep current}Response (200):
{ "id": "660e8400-...", "instance_url": "https://git.example.com", "bot_username": "mq-bot", "token_type": "pat", "updated_at": "2026-08-04T13:00:00Z"}DELETE /api/v1/connections/:id
Section titled “DELETE /api/v1/connections/:id”Remove a forge connection. All repos managed under this connection are cascade-deleted.
DELETE /api/v1/connections/:idAuthorization: Bearer <token>Response (204): empty body
POST /api/v1/connections/:id/discover
Section titled “POST /api/v1/connections/:id/discover”List repositories accessible by this connection’s stored credential (decrypted server-side only). The token is never returned or logged.
POST /api/v1/connections/:id/discoverAuthorization: Bearer <token>Response (200):
{ "repos": [ { "full_name": "owner/repo", "name": "repo", "owner": "owner", "html_url": "https://git.example.com/owner/repo", "private": false, "default_branch": "main" } ]}GET /api/v1/user/repos
Section titled “GET /api/v1/user/repos”List the authenticated user’s managed repositories with the complete safe view. Includes connection details, wake mode, provisioning status, and reconcile state.
GET /api/v1/user/reposAuthorization: Bearer <token>Response (200):
[ { "id": "770e8400-...", "name": "owner/repo", "base_branch": "main", "status_context": "merge-queue", "merge_style": "squash", "automerge": false, "instance_url": "https://git.example.com", "forge_conn_id": "660e8400-...", "wakeup_mode": "poll", "connection": { "bot_username": "mq-bot", "instance_url": "https://git.example.com", "token_type": "pat" }, "provisioning": { "webhook": { "status": "not_required" }, "protection": { "status": "configured", "id": "..." }, "last_provisioned_at": "2026-01-01T00:00:00Z" }, "reconcile": { "ran_at": "2026-01-01T00:00:00Z", "ok": true, "error": null }, "created_at": "2026-08-04T12:00:00Z", "updated_at": "2026-08-04T13:00:00Z" }]Complete field reference:
| Field | Type | Description |
|---|---|---|
id |
string | Unique repo identifier |
name |
string | Repository slug (owner/repo) |
base_branch |
string | Base branch for the queue |
status_context |
string | CI status context (default merge-queue) |
merge_style |
string | Merge strategy: merge, squash, rebase |
automerge |
boolean | Whether Forgejo auto-merge is enabled |
instance_url |
string | Forge instance URL |
forge_conn_id |
string | Foreign key to forge_connections |
wakeup_mode |
string | poll (cron) or webhook (low-latency) |
connection |
object or null | Connection details (token fields stripped) |
provisioning |
object | Provisioning state: webhook, protection, last_provisioned_at |
provisioning.webhook.status |
string | configured, not_required, needs_admin, not_configured, error |
provisioning.protection.status |
string | configured, needs_admin, not_configured, error |
provisioning.last_provisioned_at |
string or null | ISO timestamp of last provisioning attempt |
reconcile.ran_at |
string or null | Last reconcile run timestamp |
reconcile.ok |
boolean or null | Whether the last reconcile succeeded |
reconcile.error |
string or null | Error message on failure |
created_at |
string | Record creation timestamp |
updated_at |
string | Record update timestamp |
POST /api/v1/user/repos
Section titled “POST /api/v1/user/repos”Enroll a new repository in the merge queue.
Preferred form: Provide an existing connection_id.
POST /api/v1/user/reposAuthorization: Bearer <token>Content-Type: application/json
{ "connection_id": "660e8400-...", "repo_slug": "owner/repo", "base_branch": "main", "status_context": "merge-queue", "merge_style": "squash", "automerge": false, "wakeup_mode": "poll"}Legacy form (deprecated): Provide inline forge credentials. The endpoint creates or reuses a connection internally.
POST /api/v1/user/reposAuthorization: Bearer <token>Content-Type: application/json
{ "forge_instance_url": "https://git.example.com", "token": "ghp_xxxxxxxxxxxx", "bot_login": "mq-bot", "repo_slug": "owner/repo", "base_branch": "main"}Response (201 — created):
{ "id": "770e8400-...", "name": "owner/repo", "base_branch": "main", "status_context": "merge-queue", "merge_style": "squash", "automerge": false, "instance_url": "https://git.example.com", "forge_conn_id": "660e8400-...", "provisioning": { "webhook": { "status": "not_required" }, "protection": { "status": "configured" }, "last_provisioned_at": "2026-01-01T00:00:00Z" }}Response (409 — duplicate):
{ "error": { "code": "repo_already_managed", "message": "Repo owner/repo is already managed on this forge instance", "existing_tenant_id": "550e8400-...", "existing_managed_repo_id": "770e8400-..." }}Default values:
| Field | Default |
|---|---|
base_branch |
main |
status_context |
merge-queue |
merge_style |
squash |
automerge |
false |
wakeup_mode |
poll |
Provisioning: Enrollment triggers best-effort provisioning (webhook registration + branch protection). Failures do not block enrollment — per-item statuses surface in the dashboard.
PATCH /api/v1/user/repos/:id
Section titled “PATCH /api/v1/user/repos/:id”Update the wake mode for a managed repository. Returns the full safe view.
PATCH /api/v1/user/repos/:idAuthorization: Bearer <token>Content-Type: application/json
{ "wakeup_mode": "webhook"}Valid values: poll or webhook.
When switching to
webhookmode, provisioning runs automatically to register the webhook.
Response (200): full repo safe view (same as GET /api/v1/user/repos).
DELETE /api/v1/user/repos/:id
Section titled “DELETE /api/v1/user/repos/:id”Remove a managed repository.
DELETE /api/v1/user/repos/:idAuthorization: Bearer <token>Response (204): empty body
POST /api/v1/user/repos/:id/provision
Section titled “POST /api/v1/user/repos/:id/provision”Re-run provisioning for a repository. Re-attempts webhook registration and branch protection setup.
POST /api/v1/user/repos/:id/provisionAuthorization: Bearer <token>Response (200):
{ "provisioning": { "webhook": { "status": "configured" }, "protection": { "status": "configured" }, "last_provisioned_at": "2026-01-01T00:00:00Z" }}POST /api/v1/user/repos/:id/reconcile
Section titled “POST /api/v1/user/repos/:id/reconcile”Trigger a manual reconcile for a repository. Runs the merge queue algorithm immediately and persists the outcome.
POST /api/v1/user/repos/:id/reconcileAuthorization: Bearer <token>Response (200 — success):
{ "reconcile": { "ran_at": "2026-01-01T00:00:00Z", "ok": true, "error": null }}Response (500 — failure):
{ "reconcile": { "ran_at": "2026-01-01T00:00:00Z", "ok": false, "error": "reconcile failed" }}GET /api/v1/user/queue/status
Section titled “GET /api/v1/user/queue/status”Per-repo queue health snapshot. Returns the complete safe view for each repo plus live lease state.
GET /api/v1/user/queue/statusAuthorization: Bearer <token>Response (200):
{ "queue_name": "merge-queue", "repos": [ { "id": "770e8400-...", "name": "owner/repo", "base_branch": "main", "status_context": "merge-queue", "merge_style": "squash", "automerge": false, "instance_url": "https://git.example.com", "forge_conn_id": "660e8400-...", "wakeup_mode": "poll", "connection": { "bot_username": "mq-bot", "instance_url": "https://git.example.com", "token_type": "pat" }, "provisioning": { "webhook": { "status": "not_required" }, "protection": { "status": "configured" }, "last_provisioned_at": "2026-01-01T00:00:00Z" }, "reconcile": { "ran_at": "2026-01-01T00:00:00Z", "ok": true, "error": null }, "lease_state": "idle" } ]}| Field | Type | Description |
|---|---|---|
queue_name |
string | Queue name (usually “merge-queue”) |
repos[] |
array | Per-repo health snapshot |
repos[].lease_state |
string | idle, held, or unknown |
| (other fields) | — | Same complete safe view as GET /api/v1/user/repos |
Admin endpoints (X-Admin-Key auth)
Section titled “Admin endpoints (X-Admin-Key auth)”All admin endpoints require X-Admin-Key header. Admin endpoints bypass tenant scoping.
GET /api/v1/admin/repos
Section titled “GET /api/v1/admin/repos”List all repositories across all tenants. Supports search, pagination, and returns the full view with lease state and reconcile data.
GET /api/v1/admin/repos?q=owner/repo&limit=50&offset=0X-Admin-Key: <admin-key>| Parameter | Type | Description |
|---|---|---|
q |
string | Search filter on repo name (ILIKE, max 200 chars) |
limit |
int | Page size, capped at 100 |
offset |
int | Pagination offset |
Response (200):
{ "repos": [ { "repo": "owner/repo", "base_branch": "main", "status_context": "merge-queue", "merge_style": "squash", "automerge": false, "instance_url": "https://git.example.com", "tenant_id": "550e8400-...", "owner": "550e8400-...", "bot_username": "mq-bot", "created_at": "2026-08-04T12:00:00Z", "updated_at": "2026-08-04T13:00:00Z", "last_reconcile_at": "2026-01-01T00:00:00Z", "last_reconcile_ok": true, "last_error": null, "lease_state": "idle" } ], "limit": 50, "offset": 0, "total": 142}GET /api/v1/admin/connections
Section titled “GET /api/v1/admin/connections”List all connections across all tenants. Token fields are excluded.
GET /api/v1/admin/connectionsX-Admin-Key: <admin-key>Response (200):
{ "connections": [ { "id": "660e8400-...", "tenant_id": "550e8400-...", "instance_url": "https://git.example.com", "bot_username": "mq-bot", "token_type": "pat", "created_at": "2026-08-04T12:00:00Z" } ]}GET /api/v1/admin/stats
Section titled “GET /api/v1/admin/stats”Actionable operational counts for the admin console overview.
GET /api/v1/admin/statsX-Admin-Key: <admin-key>Response (200):
{ "repos_total": 42, "tenants_total": 3, "per_tenant": { "550e8400-...": 25, "550e8401-...": 17 }, "last_reconciles": [ { "ok": true, "ran_at": "2026-01-01T00:00:00Z" } ]}| Field | Type | Description |
|---|---|---|
repos_total |
int | Total managed repositories across all tenants |
tenants_total |
int | Total tenants |
per_tenant |
object | Per-tenant repo count map (tenant_id → count) |
last_reconciles |
array | Most recent reconcile outcomes (last 20) |
Forge endpoints
Section titled “Forge endpoints”POST /api/v1/forge/repos
Section titled “POST /api/v1/forge/repos”List public repositories accessible with provided forge credentials. Unauthenticated — the caller provides their own credentials in the JSON body. Used by the dashboard to browse repos before enrolling.
POST /api/v1/forge/reposContent-Type: application/json
{ "instance_url": "https://git.example.com", "username": "mq-bot", "token": "gho_xxxxxxxxxxxx"}Response (200):
{ "repos": [ { "full_name": "owner/repo", "name": "repo", "owner": "owner", "html_url": "https://git.example.com/owner/repo", "private": false, "default_branch": "main" } ]}Note: GET on this endpoint is intentionally unsupported — returns 404.
Webhook endpoint
Section titled “Webhook endpoint”POST /api/v1/webhooks/forgejo
Section titled “POST /api/v1/webhooks/forgejo”Receives webhook events from Forgejo/Gitea/Codeberg. No API key required — security is enforced via HMAC-SHA256 signature verification.
POST /api/v1/webhooks/forgejoX-Hub-Signature-256: sha256=<hex_digest>Content-Type: application/jsonSupported events: push, pull_request, issue_comment, release.
Response codes
Section titled “Response codes”| Code | Meaning |
|---|---|
| 200 | Event processed successfully |
| 400 | Bad request body or missing fields |
| 401 | HMAC signature verification failed |
| 404 | Repo not managed by gondolier |
| 409 | Lease held by another worker (event dropped) |
| 500 | Internal server error |
A 409 means the lease is currently being processed. The sender should retry with exponential backoff.
Legacy tenant-scoped endpoints
Section titled “Legacy tenant-scoped endpoints”The following endpoints use the X-API-Key header with tenant path (/api/v1/tenants/:id/...). They are the original API surface; the dashboard endpoints above (/api/v1/user/*, /api/v1/connections/*) are the preferred paths.
POST /api/v1/tenants
Section titled “POST /api/v1/tenants”Create a new tenant. Returns the raw API key (only sent once).
POST /api/v1/tenantsContent-Type: application/json
{ "name": "acme-corp", "api_key": "rk_live_abc123..."}Response (201):
{ "id": "550e8400-e29b-41d4-a716-446655440000", "name": "acme-corp", "api_key": "rk_live_abc123...", "created_at": "2026-08-04T12:00:00Z", "updated_at": "2026-08-04T12:00:00Z"}GET /api/v1/tenants
Section titled “GET /api/v1/tenants”Returns all tenants (admin only).
GET /api/v1/tenantsX-API-Key: <admin-key>GET /api/v1/tenants/:id
Section titled “GET /api/v1/tenants/:id”Returns a single tenant.
GET /api/v1/tenants/:idX-API-Key: <tenant-or-admin-key>PUT /api/v1/tenants/:id
Section titled “PUT /api/v1/tenants/:id”Update a tenant’s name.
PUT /api/v1/tenants/:idX-API-Key: <tenant-or-admin-key>Content-Type: application/json
{ "name": "acme-corp-updated"}DELETE /api/v1/tenants/:id
Section titled “DELETE /api/v1/tenants/:id”Delete a tenant and all associated connections and repos (CASCADE).
DELETE /api/v1/tenants/:idX-API-Key: <tenant-or-admin-key>Response (204): empty body
GET /api/v1/tenants/:id/connections
Section titled “GET /api/v1/tenants/:id/connections”Returns forge connections for the authenticated tenant.
GET /api/v1/tenants/:id/connectionsX-API-Key: <tenant-key>POST /api/v1/tenants/:id/connections
Section titled “POST /api/v1/tenants/:id/connections”Add a forge connection for the tenant. Supports dedup.
POST /api/v1/tenants/:id/connectionsX-API-Key: <tenant-key>Content-Type: application/json
{ "instance_url": "https://git.example.com", "bot_login": "mq-bot", "token": "ghp_xxxxxxxxxxxx", "token_type": "pat"}DELETE /api/v1/tenants/:id/connections/:connId
Section titled “DELETE /api/v1/tenants/:id/connections/:connId”Remove a connection. Managed repos cascade-delete.
DELETE /api/v1/tenants/:id/connections/:connIdX-API-Key: <tenant-key>GET /api/v1/tenants/:id/connections/:connId/repos
Section titled “GET /api/v1/tenants/:id/connections/:connId/repos”Returns managed repos for a connection.
GET /api/v1/tenants/:id/connections/:connId/reposX-API-Key: <tenant-key>POST /api/v1/tenants/:id/connections/:connId/repos
Section titled “POST /api/v1/tenants/:id/connections/:connId/repos”Add a managed repo to a connection.
POST /api/v1/tenants/:id/connections/:connId/reposX-API-Key: <tenant-key>Content-Type: application/json
{ "repo_slug": "owner/repo", "base_branch": "main", "status_context": "merge-queue", "merge_style": "squash"}DELETE /api/v1/tenants/:id/connections/:connId/repos/:repoId
Section titled “DELETE /api/v1/tenants/:id/connections/:connId/repos/:repoId”Remove a managed repo.
DELETE /api/v1/tenants/:id/connections/:connId/repos/:repoIdX-API-Key: <tenant-key>GET /api/v1/tenants/:id/queue
Section titled “GET /api/v1/tenants/:id/queue”Returns the current queue state for a tenant.
GET /api/v1/tenants/:id/queueX-API-Key: <tenant-key>GET /api/v1/tenants/:id/audit
Section titled “GET /api/v1/tenants/:id/audit”Returns paginated audit entries for a tenant.
GET /api/v1/tenants/:id/audit?page=1&limit=50X-API-Key: <tenant-key>| Parameter | Type | Description |
|---|---|---|
page |
int | Page number (default 1) |
limit |
int | Page size, max 100 (default 50) |
Token security
Section titled “Token security”- Tenant forge tokens are encrypted via envelope encryption at rest (
enc:v1:). - The encrypted token is never returned by any API response.
- The encrypted token is never logged in plaintext.
- The token is decrypted only in-memory for the duration of a single operation.
- Admin endpoints never return token fields.