The core exposes 19 routes across nine routers in
core/src/api/v1/. This page is derived from those routers, which are
the only source of truth: the core has no hand-written specification.
Every router is mounted under /v1 on an application created with
root_path="/api", so the reverse proxy publishes the routes as
/api/v1/…. Paths below are written with that prefix.
The authentication rule
Authentication is a per-route dependency, not middleware. Every
route under /v1 except GET /v1/auth/config declares the
authentication dependency, or one of the two role wrappers built on it.
A router added without one of these is public.
The dependency behaves in one of two ways:
-
Blank
oidc.issuer_url(the checked-in default) — authentication is disabled. Every request resolves to a fixed local development identity, provisioned as a real row in the users table with the top role of both groups. -
oidc.issuer_urlset — the dependency requires anAuthorization: Bearer <jwt>, validates it against the issuer’s JWKS, and maps the(issuer, subject)pair to a users row, creating it on first sight. A missing or invalid token is401with aWWW-Authenticate: Bearerchallenge.
Two independent role groups gate the routes, and both live in the core database:
-
The operation role (
developer<devops) gates Infrastructure as Code work. Falling short is403 Requires operation role '<role>' or higher. -
The panel role (
viewer<editor<admin, nullable) gates/v1/admin/*. Falling short is403 Requires admin-panel role '<role>' or higher.
Beyond the role check, a per-session access check applies: the owner
may always read and write; a non-owner with any panel role may read only; everyone else
receives 403 Not the session owner. See
Roles and permissions
for the full matrix.
The whole surface is summarized here and then described router by router.
| Route | Requires | Returns |
|---|---|---|
|
nothing (public) |
|
|
any signed-in user |
|
|
any signed-in user |
|
|
operation |
|
|
operation |
|
|
operation |
|
|
read access to the session |
|
|
operation |
|
|
operation |
|
|
operation |
|
|
any signed-in user |
|
|
read access to the session |
|
|
panel |
|
|
panel |
|
|
panel |
|
|
panel |
|
|
panel |
|
|
any signed-in user |
|
|
any signed-in user |
|
Infrastructure as Code
Prefix /v1/iac. Three routes, one per
operation. All three are asynchronous: they validate the request,
resolve or create the session, schedule the pipeline as a background
task, and answer 202 Accepted with nothing but the session id.
Progress arrives over the event stream.
{"session_id": "917d0485-a0a2-4c34-8f33-a89d28aba9b0"}
| Route | Minimum role | Notes |
|---|---|---|
|
|
Generates, validates, and prepares IaC from a natural-language query. |
|
|
Detects and remediates drift. |
|
|
Applies the plan pinned by the session’s last successful generate round. No re-plan happens at apply time. |
generate and drift share a request body. Exactly one of repo_uri
(a first call, which opens a session) or session_id (an iteration
call on an existing session) must be present; supplying both or
neither is a validation error.
| Field | First call | Meaning |
|---|---|---|
|
required |
The user’s query for this call. At least one character. |
|
required |
|
|
required |
Cloud scope: Azure subscription id, GCP project id, AWS account id, OCI compartment OCID. Kubernetes has no cloud scope, so any stable identifier does. |
|
required |
Which cloud the operation targets. |
|
optional |
Repository-relative path to the IaC root, for example
|
|
omit |
On an iteration call, the session to continue. Only |
|
optional |
|
apply takes only session_id, which is required.
Failures on these three routes:
-
400— the repository URI was rejected as unreachable or not allowed. -
403— the operation role is too low, or the caller is not the owner of the session named bysession_id. -
404— an iteration call referenced an unknown session. -
409—applyonly, and only when the session’s plan is locked:Session <id> is blocked; apply is not allowed. -
422— the body failed the validation rules above.
A 202 is a promise that a round was scheduled, not that it
succeeded. If the session is already running, or already finished, the
background runner cannot take the session lock and exits without
starting — the 202 has already been sent. Everything after the 202
is visible only on the event stream and in the session detail.
Events
Prefix /v1/events. One route.
GET /api/v1/events/subscribe/{session_id} streams a session’s
progress as Server-Sent Events. It requires a signed-in caller with
read access to the session — the owner, or any user with a panel role.
The response is text/event-stream with Cache-Control: no-cache,
Connection: keep-alive, and X-Accel-Buffering: no, the last of
which stops the reverse proxy from buffering the stream. The first
line is a : keepalive comment; every subsequent event is one JSON
object:
data: {"status_msg": "GENERATING", "detail": {"message": "..."}}
status_msg is the session’s current status name. The message is
sanitized by removing double quotes before it is embedded. The server
polls the session’s status every five seconds and closes the stream
when the status becomes COMPLETED, UNCOMPLETED (a rejected round),
or FAILED, or when the configured iteration ceiling
(orchestration.max_session_events_iteration) is reached. Reaching
the ceiling closes the connection but preserves the session, so a
client may reconnect.
Both the token and session access are checked once, at connect time.
An unknown session is 404; no read access is 403.
The web application consumes this with fetch and a
ReadableStream rather than EventSource, because EventSource
cannot send an Authorization header. See
Follow a session.
Repository operations
Prefix /v1/repository. Three routes, all
requiring operation role developer.
PUT /api/v1/repository/pr creates a pull request from the session’s
working branch. The body is {"session_id": "<uuid>"}. The caller
must own the session. On success the answer is 201 with the pull
request:
{"id": 42, "url": "https://.../pull/42", "status": "active"}
PUT /api/v1/repository/pr/merge merges the session’s most recently
opened pull request into the default branch at the git provider. The
body is the same {"session_id": "<uuid>"}. The caller must own the
session. Success is 204 with no content. A locked session is 409
Session <id> is blocked; PR merge is not allowed., an unknown session
or a session with no pull requests is 404.
POST /api/v1/repository/parse clones a repository and returns the
Terraform root-module directories it found, as POSIX paths relative to
the repository root. The body is {"repo_uri": "<uri>"} and the same
https-only, no-password-or-token rules apply. This route takes no session, so
there is no ownership check — the wizard calls it as soon as a
repository URL is entered.
{"roots": [".", "environments/dev", "environments/prod"]}
A URI that is not https:// or embeds a password or token is 422,
one that git ls-remote cannot reach is 400, and a failed clone or
scan is 502. The 400 and the clone 502 carry the fixed detail
"Repository is not reachable or access was denied."; git’s own output
is only logged. The
detection rules behind the answer are in
Repository layout.
Authentication
Prefix /v1/auth. Two routes.
GET /api/v1/auth/config is the only public route under /v1,
deliberately so: the single-page application has to read it before it
can log in at all. Outside /v1, the framework’s interactive
documentation — /api/docs, /api/redoc, and /api/openapi.json —
is served without authentication as well. It returns the four
non-secret OIDC settings, taken straight from config.yaml:
{
"issuer_url": "",
"client_id": "",
"audience": "",
"scope": "openid profile email",
"artifact_metadata_header_prefix": "x-amz-meta-"
}
A blank issuer_url tells the browser that authentication is disabled
and it should use the local development identity. See
Enable authentication.
artifact_metadata_header_prefix is not configurable. It is derived
from storage.provider — x-amz-meta- for S3 and RustFS,
x-ms-meta- for an Azure storage account — and is how the browser
knows which header names carry an artifact’s metadata when it reads one
from a pre-signed URL. The core never reads that metadata itself, so
this is the only way the client can learn it. See
Networking and TLS
for the CORS rules that must expose those headers.
POST /api/v1/auth/authorize asks whether the caller may operate on a
cloud project. It requires a signed-in user and takes cloud,
project_name, and environment in the body. It is the only place
the core consults the authorization sidecar, forwarding the three
values plus the caller’s e-mail (falling back to the token subject) to
the sidecar’s POST /v1/check.
{
"result": true,
"message": "Authorized",
"portalUrl": "https://portal.azure.com/..."
}
result is the sidecar’s authorized flag, message is its reason
or a generated sentence, and portalUrl renames the sidecar’s
portal_url. When the sidecar is disabled — the shipped default — the
core answers authorized without calling anything. When it is enabled
and unreachable the answer is 502 or 504, and when it answers with
a 4xx or 5xx the answer is 502. It is never an allow. See
Authorization sidecar API.
Users
Prefix /v1/users. One route.
GET /api/v1/users/me returns the authenticated caller’s identity and
roles, and is what the browser calls to learn what to render:
{
"id": 1,
"email": "dev@kumoss.local",
"display_name": "Local Developer",
"operation_role": "devops",
"panel_role": "admin"
}
id is the database primary key, not the token subject. email and
display_name may be null. panel_role is null for a user with no
admin-panel access.
Sessions
Prefix /v1/sessions. Two routes, both open to
any signed-in user.
GET /api/v1/sessions/list returns a page of session summaries. The list
is always scoped to the caller’s own sessions — there is no
parameter that widens it.
| Parameter | Default | Meaning |
|---|---|---|
|
|
One-based page number, minimum 1. |
|
|
Items per page, 1 to 100. |
|
none |
Filter by operation type. |
|
none |
Filter by the session’s most recent status. |
|
none |
Partial match on the first query, the workspace URI, or the session id. Up to 200 characters. |
The envelope carries items, total, page, page_size, and
total_pages. Each item resolves server-side everything the list view
renders: uuid, username, operation, provider, first_query,
workspace (URI, branch, root path), current_status, the
in_flight and is_blocked flags, and the timestamps. username is
the owning user’s e-mail, or user-<id> when the user has none; the
name is historical, and it is a response field only — there is no
username query parameter.
GET /api/v1/sessions?id={session_id} returns the full session
aggregate: the summary fields, its scope_id, and one entry in
rounds per generation round. Each round carries its own statuses,
its reports, compliance_checks, plans, and code_changes
artifacts as client-fetchable URLs, oldest first, and its
pull_requests. Each compliance_checks entry also carries the audit’s
passed verdict; the list is empty when the round was not audited.
Pass include_history=true to also populate history with the
session’s conversation turns. That variant always reads fresh from the
database, so status polling should stay on the default. The caller
must be the owner or hold a panel role; an unknown session is 404.
Admin
Prefix /v1/admin. Five routes. The whole router
requires a panel role, so viewer is the floor for every route in it; three routes raise the
floor further.
| Route | Panel role | Returns |
|---|---|---|
|
|
A page of session summaries across all users. |
|
|
Any session’s full aggregate. |
|
|
The session’s new lock state. |
|
|
A page of users with their roles. |
|
|
The updated user. |
GET /api/v1/admin/sessions/list takes the same page, page_size,
operation, status, and search parameters as the caller-scoped
list, plus user_email, which partial-matches the owning user’s
e-mail and accepts up to 254 characters. The response envelope is
identical.
GET /api/v1/admin/sessions?id={session_id} is the unrestricted
counterpart of the session detail route: same include_history
parameter, same response, but no ownership check beyond the panel
role. An unknown session is 404.
PATCH /api/v1/admin/sessions/{session_id}/toggle_lock takes
{"locked": true} or {"locked": false} and answers with the
resulting state. true blocks both apply and pull-request merge on
that session; false releases them. An unknown session is 404.
{"uuid": "917d0485-a0a2-4c34-8f33-a89d28aba9b0", "is_blocked": true}
GET /api/v1/admin/users takes page, page_size, and a search
that partial-matches e-mail or display name, up to 254 characters.
Each item extends the /users/me shape with the issuer, the
subject, and created_at.
PUT /api/v1/admin/users/{user_id}/roles is a full-state
assignment, not a patch: the body is
{"operation_role": "devops", "panel_role": "editor"} and whatever it
omits is cleared. panel_role is optional and setting it to null
removes all admin-panel access. user_id is the database primary key.
An unknown user is 404. One case is refused outright: a panel admin
removing their own admin role is 409 A panel admin cannot remove
their own admin role.
Note what is not here. There is no route to delete a user or a session, and a panel role never grants the right to act on another user’s session — continuing it, opening or merging its pull request, and applying it all remain owner-only.
Mapping
Prefix /v1/mapping. One route, open to any
signed-in user.
POST /api/v1/mapping/resolve is a passthrough to the mapping
sidecar. It takes identifier (required) plus an optional
terraform_provider the caller already knows, and returns what the
mapper knows about that identifier:
{
"repo_url": "https://github.com/me/my-iac.git",
"identifier": "https://github.com/me/my-iac.git",
"terraform_provider": null,
"scope_id": null
}
Only repo_url is guaranteed. terraform_provider and scope_id are
the mapper’s best effort, and null means "unknown, ask the user" —
never "there is none". The wizard skips its provider and scope questions
for whichever of them comes back non-null. A body with extra keys is
422; a sidecar that times out is 504, and one that is unreachable,
answers off-contract, or names a provider outside the enum is 502.
The sidecar is not reachable from the browser; this route is how the web application gets at it. When mapping is disabled in the system configuration the core’s client answers with an identity passthrough, so a default deployment works without a configured mapper. See Mapping sidecar API.
Notifications
Prefix /v1/notifications. One route,
open to any signed-in user.
POST /api/v1/notifications submits a user-originated notification —
today, the support request typed into the web application’s header —
and the core relays it to the notifications sidecar. The body mirrors
the sidecar contract’s NotificationRequest with one deliberate
difference: it has no audience field. The core derives the
audience server-side from the caller and the users store, so the
browser cannot choose recipients. Unknown fields are rejected.
| Field | Required | Constraint |
|---|---|---|
|
yes |
Event category, dotted lowercase, for example
|
|
yes |
One of |
|
yes |
1 to 256 characters. |
|
yes |
1 to 16384 characters. |
|
no |
Up to 32 objects, each |
|
no |
Free-form object. |
Success is 202 with the sidecar’s delivery id:
{"delivery_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"}
Unlike the pipeline’s own best-effort notifications, this route tells
the caller when nothing was delivered: 502 Notification was not
delivered. if the sidecar did not accept it, and 503 Notifications
are disabled. if the sidecar is switched off in the configuration.
Next steps
-
Roles and permissions for the complete permission matrix behind the role columns above.
-
HTTP APIs for the sidecar contracts the core calls.
-
Make a request for the same flow from the web application.