Skip to content

API Reference

Status: Authoritative API reference.

Cross-references: ARCHITECTURE.md (system overview), SECURITY.md (authentication model), UX DESIGN (dashboard/admin contract).

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>

All error responses use a consistent envelope:

{
"code": "invalid_key",
"message": "Invalid API key"
}
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

These endpoints require a valid Supabase session token.

List the authenticated user’s forge connections. Token fields are never returned.

GET /api/v1/connections
Authorization: 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"
}
]

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/connections
Authorization: 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"
}

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/:id
Authorization: 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"
}

Remove a forge connection. All repos managed under this connection are cascade-deleted.

DELETE /api/v1/connections/:id
Authorization: Bearer <token>

Response (204): empty body

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/discover
Authorization: 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"
}
]
}

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/repos
Authorization: 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

Enroll a new repository in the merge queue.

Preferred form: Provide an existing connection_id.

POST /api/v1/user/repos
Authorization: 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/repos
Authorization: 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.

Update the wake mode for a managed repository. Returns the full safe view.

PATCH /api/v1/user/repos/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"wakeup_mode": "webhook"
}

Valid values: poll or webhook.

When switching to webhook mode, provisioning runs automatically to register the webhook.

Response (200): full repo safe view (same as GET /api/v1/user/repos).

Remove a managed repository.

DELETE /api/v1/user/repos/:id
Authorization: Bearer <token>

Response (204): empty body

Re-run provisioning for a repository. Re-attempts webhook registration and branch protection setup.

POST /api/v1/user/repos/:id/provision
Authorization: Bearer <token>

Response (200):

{
"provisioning": {
"webhook": { "status": "configured" },
"protection": { "status": "configured" },
"last_provisioned_at": "2026-01-01T00:00:00Z"
}
}

Trigger a manual reconcile for a repository. Runs the merge queue algorithm immediately and persists the outcome.

POST /api/v1/user/repos/:id/reconcile
Authorization: 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"
}
}

Per-repo queue health snapshot. Returns the complete safe view for each repo plus live lease state.

GET /api/v1/user/queue/status
Authorization: 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

All admin endpoints require X-Admin-Key header. Admin endpoints bypass tenant scoping.

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=0
X-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
}

List all connections across all tenants. Token fields are excluded.

GET /api/v1/admin/connections
X-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"
}
]
}

Actionable operational counts for the admin console overview.

GET /api/v1/admin/stats
X-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)

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/repos
Content-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.


Receives webhook events from Forgejo/Gitea/Codeberg. No API key required — security is enforced via HMAC-SHA256 signature verification.

POST /api/v1/webhooks/forgejo
X-Hub-Signature-256: sha256=<hex_digest>
Content-Type: application/json

Supported events: push, pull_request, issue_comment, release.

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.


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.

Create a new tenant. Returns the raw API key (only sent once).

POST /api/v1/tenants
Content-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"
}

Returns all tenants (admin only).

GET /api/v1/tenants
X-API-Key: <admin-key>

Returns a single tenant.

GET /api/v1/tenants/:id
X-API-Key: <tenant-or-admin-key>

Update a tenant’s name.

PUT /api/v1/tenants/:id
X-API-Key: <tenant-or-admin-key>
Content-Type: application/json
{
"name": "acme-corp-updated"
}

Delete a tenant and all associated connections and repos (CASCADE).

DELETE /api/v1/tenants/:id
X-API-Key: <tenant-or-admin-key>

Response (204): empty body

Returns forge connections for the authenticated tenant.

GET /api/v1/tenants/:id/connections
X-API-Key: <tenant-key>

Add a forge connection for the tenant. Supports dedup.

POST /api/v1/tenants/:id/connections
X-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/:connId
X-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/repos
X-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/repos
X-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/:repoId
X-API-Key: <tenant-key>

Returns the current queue state for a tenant.

GET /api/v1/tenants/:id/queue
X-API-Key: <tenant-key>

Returns paginated audit entries for a tenant.

GET /api/v1/tenants/:id/audit?page=1&limit=50
X-API-Key: <tenant-key>
Parameter Type Description
page int Page number (default 1)
limit int Page size, max 100 (default 50)

  • 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.