REST API
The gateway exposes a JSON control-plane API under /api/ on its single HTTP listener (port 8080 in the default Compose setup). The dashboard is a pure frontend over this API — anything the dashboard does, you can do with curl.
The API is pre-1.0 and subject to change. Routes documented here are the ones registered in the gateway source today (internal/api).
Authentication
All routes except /api/auth/* require a valid gig_session cookie, issued by the gateway after a successful OIDC login. There is no API-key authentication for the control plane today — see Authentication for the full login flow.
Two additional layers apply on top of the session:
- Admin-only routes (marked below) require the real user's role to be
admin. Roles come from the IdP; there is no local role editing. - Impersonation is read-only. While an admin is impersonating a user ("view-as"), every non-
GET/HEADrequest returns403, with one carve-out:DELETE /api/admin/impersonateto stop impersonating.
The control-plane API is only mounted when OIDC is configured; without it the gateway disables /api/ entirely (a descriptive 404 tells you which env vars to set) while the MCP data plane keeps working.
Profile MCP endpoints (/mcp/p/<slug>) use per-profile bearer tokens, not sessions. That is the data plane, not this API — see Profiles.
Conventions
- Responses are
application/jsonwithCache-Control: no-store. - Request bodies are strict JSON: unknown fields are rejected, and bodies are capped at 64 KiB (
413beyond that). - Errors use a uniform envelope, including bare 404/405s from the router:
{
"error": {
"code": "not_found",
"message": "profile not found"
}
}
| Code | Typical status |
|---|---|
unauthenticated | 401 |
forbidden | 403 |
impersonating | 403 (mutation attempted during view-as) |
not_found | 404 |
method_not_allowed | 405 |
invalid | 400 / 413 |
conflict | 409 |
internal | 500 |
install_failed / uninstall_failed | 502 |
Mutations write synchronous rows to the audit log.
Auth routes
Unauthenticated — these are the authentication. Mounted only when OIDC is configured.
| Method | Path | Purpose |
|---|---|---|
GET | /api/auth/login | Start the OIDC flow (sets a signed, 10-minute flow cookie carrying state/nonce/PKCE). |
GET | /api/auth/callback | OIDC redirect target; on success sets the gig_session cookie. |
POST | /api/auth/logout | Clear the session (local logout). |
Session
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/me | session | The effective user; while impersonating, also the real (admin) identity. |
{
"user": { "id": 2, "email": "user@example.com", "display_name": "User", "role": "user" },
"impersonating": true,
"real_user": { "id": 1, "email": "admin@example.com", "display_name": "Admin", "role": "admin" }
}
real_user is omitted when not impersonating.
Profiles
A profile is one MCP endpoint (/mcp/p/<slug>) with its own bearer token and server set. Users see their own profiles; admins see all. Requesting someone else's profile by ID returns 404 (not 403) so existence never leaks.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/profiles | session | List profiles (own; all if admin). |
POST | /api/profiles | session | Create a profile. Returns the bearer token plaintext — exactly once. |
GET | /api/profiles/{id} | owner or admin | Profile detail incl. endpoint and servers. Never the token. |
PATCH | /api/profiles/{id} | owner or admin | Rename ({"name": "..."}). |
DELETE | /api/profiles/{id} | owner or admin | Delete; cascades server assignments and tears down the profile's MCP runtime. 204. |
PUT | /api/profiles/{id}/servers | owner or admin | Replace the full server list ({"servers": ["echo"]}). Every name must be an installed server. Invalidates the runtime. |
POST | /api/profiles/{id}/token | owner or admin | Rotate the bearer token; returns the new plaintext once. Does not restart the runtime. |
Create body:
{ "name": "My profile", "slug": "my-profile" }
name: 1–128 characters.slug: must match^[a-z0-9][a-z0-9-]{1,62}$;409 conflictif taken.
Profile object:
{
"id": 1,
"slug": "my-profile",
"name": "My profile",
"user_id": 2,
"endpoint": "/mcp/p/my-profile",
"servers": ["echo"],
"token": "…only present on create and rotate…"
}
Credentials
Credentials are write-only: the secret is envelope-encrypted into the vault on PUT and never returned. List responses contain metadata only. Credentials are scoped per user (tenant).
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/credentials | session | List your credentials — metadata only, never the secret. |
PUT | /api/credentials/{server} | session | Encrypt and upsert a secret for a server. 204. |
DELETE | /api/credentials/{server} | session | Delete (idempotent). 204. |
{server} must match ^[a-z0-9][a-z0-9_-]{0,63}$. A credential may be created before the server is installed.
PUT body:
{
"secret": "ghp_…",
"inject_header": "Authorization",
"inject_format": "Bearer {token}",
"placeholder": "PLACEHOLDER_TOKEN",
"allowed_hosts": ["api.github.com", "*.example.com"]
}
| Field | Required | Constraints |
|---|---|---|
secret | yes | Non-empty; up to 64 KiB (service-account JSON fits). |
inject_header | no | HTTP header name chars, ≤ 64 chars. |
inject_format | no | ≤ 256 chars; must contain {token}. |
placeholder | no | 8–256 chars. |
allowed_hosts | no | ≤ 64 entries; each ^(\*\.)?[a-z0-9.-]+$, ≤ 253 chars. |
inject_header, inject_format, placeholder, and allowed_hosts are transitional: once registry manifests become the source of injection config, the body shrinks to {secret} and these fields go away. See Manifests.
Servers
Server install/uninstall pulls digest-pinned images from the signed registry index — see Registry overview.
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/servers | session | List installed servers ([{"id": 1, "name": "echo"}]). |
POST | /api/servers/install | admin | Install by registry ref: {"ref": "name"}. 201 with the server, 502 install_failed on error. |
DELETE | /api/servers/{name} | admin | Uninstall; removes the server from all profiles and invalidates their runtimes. Idempotent — re-run to retry cleanup. 204. |
If the gateway is running without a registry installer configured, install/uninstall return 501.
Users
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/users | admin | Read-only user list. Roles come from the IdP; there is no local user editing. |
Impersonation
Config-only "view-as": the admin sees the target's profiles, servers, and audit view, but cannot mutate anything or touch secrets. Time-boxed (max 60 minutes), and loudly audited — the event is attributed to the target so it appears in their own audit view.
| Method | Path | Auth | Purpose |
|---|---|---|---|
POST | /api/admin/impersonate | admin | Start: {"user_id": 2, "ttl_minutes": 30}. TTL is clamped to 60 minutes. You cannot impersonate yourself. 204. |
DELETE | /api/admin/impersonate | admin | Stop. The only mutation allowed while impersonating — switching targets requires stopping first. 204. |
Audit
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /api/audit | session | Audit events, newest first, keyset-paginated. |
Query parameters:
| Param | Description |
|---|---|
before | Return events with ID lower than this (from the previous page's next_before). |
limit | Page size, 1–200; default 50. |
user_id | Filter by user — honored only for non-impersonating admins. |
Visibility: non-admins always see only their own events; an impersonating admin sees the target's events; a pure admin sees everything (optionally filtered by user_id).
{
"events": [
{
"id": 42,
"ts": "2026-06-07T12:00:00Z",
"kind": "admin",
"user_id": 2,
"profile_id": 1,
"server": "",
"host": "",
"decision": "profile_create",
"detail": "slug=my-profile"
}
],
"next_before": 42
}
next_before is 0 when there are no further pages.