2026-10-01 23:15:57 +02:00
{
"openapi" : "3.1.0" ,
"info" : {
"title" : "Go Loose API" ,
"version" : "0.1.0" ,
"description" : "Multi-tenant API-key management and runtime authorization."
} ,
"servers" : [ { "url" : "/" } ] ,
"components" : {
"securitySchemes" : {
"session" : { "type" : "apiKey" , "in" : "cookie" , "name" : "go_loose_session" } ,
"apiKey" : { "type" : "apiKey" , "in" : "header" , "name" : "X-API-Key" } ,
"clientBasic" : { "type" : "http" , "scheme" : "basic" } ,
"userBearer" : { "type" : "http" , "scheme" : "bearer" }
} ,
"schemas" : {
"Error" : { "type" : "object" , "properties" : { "error" : { "type" : "string" } } } ,
"AuthorizationRequest" : {
"type" : "object" ,
"required" : [ "application" , "method" , "path" ] ,
"properties" : {
"tenant" : { "type" : "string" , "example" : "nmbs" } ,
"application" : { "type" : "string" , "example" : "guess" } ,
"method" : { "type" : "string" , "example" : "GET" } ,
"path" : { "type" : "string" , "example" : "/api/guesses" } ,
"host" : { "type" : "string" , "example" : "nmbs.guess.local" }
}
} ,
"AuthorizationResponse" : {
"type" : "object" ,
"properties" : {
"allowed" : { "type" : "boolean" } ,
"reason" : { "type" : "string" } ,
"tenant_id" : { "type" : "string" , "format" : "uuid" } ,
"tenant_slug" : { "type" : "string" } ,
"application_id" : { "type" : "string" , "format" : "uuid" } ,
"application_slug" : { "type" : "string" } ,
"api_key_id" : { "type" : "string" , "format" : "uuid" } ,
"api_key_name" : { "type" : "string" }
}
2026-10-04 12:24:13 +02:00
} ,
"Membership" : {
"type" : "object" ,
"description" : "A tenant role held by a user, with the capabilities it grants." ,
"properties" : {
"tenant_id" : { "type" : "string" , "format" : "uuid" } ,
"tenant_slug" : { "type" : "string" } ,
"tenant_name" : { "type" : "string" } ,
2026-10-04 16:57:30 +02:00
"role" : { "type" : "string" , "description" : "Slug of the role, which may be a role this tenant defined." } ,
"role_id" : { "type" : "string" , "format" : "uuid" , "description" : "Identifier of the role row. Send this back when assigning the role." } ,
"permissions" : { "type" : "array" , "items" : { "$ref" : "#/components/schemas/PermissionValue" } }
2026-10-04 12:24:13 +02:00
}
} ,
"Profile" : {
"type" : "object" ,
"properties" : {
"id" : { "type" : "string" , "format" : "uuid" } ,
"email" : { "type" : "string" , "format" : "email" } ,
"display_name" : { "type" : "string" } ,
"avatar_url" : { "type" : "string" , "description" : "Absolute picture URL, or an empty string when no picture is set." } ,
"memberships" : { "type" : "array" , "items" : { "$ref" : "#/components/schemas/Membership" } } ,
"is_system_administrator" : { "type" : "boolean" } ,
"name_customized" : { "type" : "boolean" , "description" : "True once the user set the display name themselves, which stops the identity provider from overwriting it." } ,
"created_at" : { "type" : "string" , "format" : "date-time" } ,
"last_login_at" : { "type" : "string" , "format" : "date-time" } ,
"profile_updated_at" : { "type" : "string" , "format" : "date-time" } ,
"avatar_updated_at" : { "type" : "string" , "format" : "date-time" }
}
} ,
"ProfileUpdate" : {
"type" : "object" ,
"description" : "Send display_name, avatar, or remove_avatar. avatar accepts raw base64 or a data URL." ,
"properties" : {
"display_name" : { "type" : "string" , "minLength" : 1 , "maxLength" : 200 } ,
"avatar" : { "type" : "string" , "description" : "Base64 or data URL of a JPEG, PNG, or GIF up to 2 MiB and 4096 by 4096 pixels." } ,
"remove_avatar" : { "type" : "boolean" }
}
} ,
2026-10-04 16:57:30 +02:00
"Role" : {
2026-10-04 12:24:13 +02:00
"type" : "object" ,
2026-10-04 16:57:30 +02:00
"description" : "A named bundle of permissions. A built-in role has no tenant_id, is visible to every tenant, and cannot be edited or deleted. A tenant may define its own." ,
"required" : [ "id" , "slug" , "name" , "system" , "permissions" ] ,
2026-10-04 12:24:13 +02:00
"properties" : {
2026-10-04 16:57:30 +02:00
"id" : { "type" : "string" , "format" : "uuid" } ,
"tenant_id" : { "type" : "string" , "format" : "uuid" , "description" : "Absent for built-in roles." } ,
"slug" : { "type" : "string" , "pattern" : "^[a-z0-9][a-z0-9-]{0,62}$" } ,
"name" : { "type" : "string" , "minLength" : 1 , "maxLength" : 100 } ,
"description" : { "type" : "string" , "maxLength" : 500 } ,
"system" : { "type" : "boolean" , "description" : "True for built-in roles, which are read only." } ,
"permissions" : { "type" : "array" , "items" : { "$ref" : "#/components/schemas/PermissionValue" } } ,
"member_count" : { "type" : "integer" , "description" : "Members currently holding the role. A role that still has members cannot be deleted." } ,
"assignable" : { "type" : "boolean" , "description" : "Whether the caller may assign this role. False when the role grants a permission the caller does not hold, which is what stops role assignment becoming a way to gain authority." }
}
} ,
"PermissionValue" : {
"type" : "string" ,
"description" : "The closed vocabulary of capabilities a role may grant. Mirrors internal/role and the role_permissions check constraint." ,
"enum" : [ "tenants.manage" , "roles.manage" , "users.manage" , "applications.manage" , "keys.manage" , "contracts.manage" , "users.read" , "console.read" , "profile.edit" ]
} ,
"PermissionEntry" : {
"type" : "object" ,
"description" : "A permission with the label and description the console renders." ,
"properties" : {
"value" : { "$ref" : "#/components/schemas/PermissionValue" } ,
2026-10-04 12:24:13 +02:00
"label" : { "type" : "string" } ,
2026-10-04 16:57:30 +02:00
"description" : { "type" : "string" }
}
} ,
"RoleRequest" : {
"type" : "object" ,
"required" : [ "name" , "permissions" ] ,
"properties" : {
"tenant_id" : { "type" : "string" , "format" : "uuid" , "description" : "Required when creating. Ignored when updating, which takes the role's own tenant." } ,
"slug" : { "type" : "string" , "pattern" : "^[a-z0-9][a-z0-9-]{0,62}$" , "description" : "Required when creating, and fixed for the life of the role." } ,
"name" : { "type" : "string" , "minLength" : 1 , "maxLength" : 100 } ,
"description" : { "type" : "string" , "maxLength" : 500 } ,
"permissions" : { "type" : "array" , "items" : { "$ref" : "#/components/schemas/PermissionValue" } }
2026-10-04 12:24:13 +02:00
}
2026-10-01 23:15:57 +02:00
}
}
} ,
"paths" : {
"/healthz" : {
"get" : {
"tags" : [
"Monitoring"
] ,
"summary" : "Liveness check" ,
"responses" : { "200" : { "description" : "Healthy" } }
}
} ,
"/install" : {
"get" : {
"tags" : [ "Installation" ] ,
"summary" : "Show the first-run installer" ,
"responses" : { "200" : { "description" : "Installer form" } , "303" : { "description" : "Already installed" } }
} ,
"post" : {
"tags" : [ "Installation" ] ,
"summary" : "Persist Google OIDC configuration and optional demo data" ,
"requestBody" : { "required" : true , "content" : { "application/x-www-form-urlencoded" : { "schema" : { "type" : "object" , "required" : [ "csrf_token" , "oidc_client_id" , "oidc_client_secret" ] , "properties" : { "csrf_token" : { "type" : "string" } , "oidc_client_id" : { "type" : "string" } , "oidc_client_secret" : { "type" : "string" , "format" : "password" } , "seed_demo" : { "type" : "boolean" } } } } } } ,
"responses" : { "303" : { "description" : "Installation completed; continue to Google OIDC login" } , "409" : { "description" : "Already installed" } , "502" : { "description" : "Google OIDC discovery failed" } }
}
} ,
"/auth/password" : {
"post" : {
"tags" : [ "User Action" ] ,
"summary" : "Authenticate a database-backed user" ,
"requestBody" : { "required" : true , "content" : { "application/x-www-form-urlencoded" : { "schema" : { "type" : "object" , "required" : [ "email" , "password" ] , "properties" : { "email" : { "type" : "string" , "format" : "email" } , "password" : { "type" : "string" , "format" : "password" } , "return" : { "type" : "string" , "format" : "uri" } } } } } } ,
"responses" : { "303" : { "description" : "Login succeeded" } , "401" : { "description" : "Invalid email or password" } , "429" : { "description" : "Temporarily rate limited" } }
}
} ,
"/api/v1/authorize" : {
"post" : {
"tags" : [
"Authorization Management"
] ,
"summary" : "Authorize an incoming API request" ,
"security" : [ { "apiKey" : [ ] } ] ,
"requestBody" : { "required" : true , "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/AuthorizationRequest" } } } } ,
"responses" : {
"200" : { "description" : "Authorized" , "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/AuthorizationResponse" } } } } ,
"401" : { "description" : "Missing or invalid key" , "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/Error" } } } } ,
"403" : { "description" : "Key not allowed for this request" , "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/AuthorizationResponse" } } } }
}
}
} ,
"/api/v1/dashboard" : {
"get" : {
"tags" : [
"Dashboard Management"
] ,
"summary" : "Get management console data" ,
2026-10-04 12:24:13 +02:00
"description" : "Requires a role that grants console.read. Each tenant and user entry carries the capability names its role grants." ,
2026-10-01 23:15:57 +02:00
"security" : [ { "session" : [ ] } ] ,
"responses" : { "200" : { "description" : "Dashboard data" } }
}
} ,
2026-10-04 12:24:13 +02:00
"/profile" : {
"get" : {
"tags" : [ "Profile" ] ,
"summary" : "Open the self-service profile screen" ,
"description" : "Available to every authenticated management session, including self-service User members." ,
"security" : [ { "session" : [ ] } ] ,
"responses" : { "200" : { "description" : "Profile screen" } , "303" : { "description" : "Redirect to login" } }
}
} ,
"/api/v1/profile" : {
"get" : {
"tags" : [ "Profile" ] ,
"summary" : "Read the caller's own profile and tenant roles" ,
"security" : [ { "session" : [ ] } ] ,
"responses" : {
"200" : { "description" : "Profile" , "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/Profile" } } } } ,
"401" : { "description" : "Authentication required" }
}
} ,
"put" : {
"tags" : [ "Profile" ] ,
"summary" : "Update the caller's display name or profile picture" ,
"security" : [ { "session" : [ ] } ] ,
"requestBody" : { "required" : true , "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/ProfileUpdate" } } } } ,
"responses" : {
"200" : { "description" : "Updated profile" , "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/Profile" } } } } ,
"400" : { "description" : "Invalid display name or unsupported picture" }
}
}
} ,
"/api/v1/avatars/{key}" : {
"get" : {
"tags" : [ "Profile" ] ,
"summary" : "Fetch a profile picture" ,
"description" : "Public on purpose so other applications can embed the picture with a plain image tag. The key is 256 bits of randomness and exposes nothing about the user." ,
"parameters" : [ { "in" : "path" , "name" : "key" , "required" : true , "schema" : { "type" : "string" } } ] ,
"responses" : {
"200" : { "description" : "Image bytes" , "content" : { "image/jpeg" : { "schema" : { "type" : "string" , "format" : "binary" } } , "image/png" : { "schema" : { "type" : "string" , "format" : "binary" } } , "image/gif" : { "schema" : { "type" : "string" , "format" : "binary" } } } } ,
"304" : { "description" : "Not modified" } ,
"404" : { "description" : "No picture for that key" }
}
}
} ,
2026-10-01 23:15:57 +02:00
"/api/v1/tenants" : {
"post" : {
"tags" : [
"Tenant Management"
] ,
"summary" : "Create a tenant" ,
"security" : [ { "session" : [ ] } ] ,
"responses" : { "201" : { "description" : "Tenant created" } }
}
} ,
"/api/v1/applications" : {
"post" : {
"tags" : [
"Application Management"
] ,
"summary" : "Register an application" ,
"security" : [ { "session" : [ ] } ] ,
"responses" : { "201" : { "description" : "Application registered" } }
}
} ,
"/api/v1/keys" : {
"post" : {
"tags" : [
"Key Management"
] ,
"summary" : "Issue an API key" ,
"security" : [ { "session" : [ ] } ] ,
"responses" : { "201" : { "description" : "Key created; plaintext secret is returned once" } }
}
} ,
"/api/v1/keys/{id}/revoke" : {
"post" : {
"tags" : [
"Key Management"
] ,
"summary" : "Revoke an API key" ,
"security" : [ { "session" : [ ] } ] ,
"parameters" : [ { "in" : "path" , "name" : "id" , "required" : true , "schema" : { "type" : "string" , "format" : "uuid" } } ] ,
"responses" : { "204" : { "description" : "Key revoked" } }
}
} ,
"/api/v1/contracts" : {
"post" : {
"tags" : [
"Contract Management"
] ,
"summary" : "Import a pasted contract or scan an OpenAPI URL" ,
"security" : [ { "session" : [ ] } ] ,
"responses" : { "201" : { "description" : "Contract imported" } }
}
} ,
"/connect/authorize" : {
"get" : {
"tags" : [
"User Action"
] ,
"summary" : "Start hosted client application login" ,
"parameters" : [
{ "in" : "query" , "name" : "response_type" , "required" : true , "schema" : { "type" : "string" , "const" : "code" } } ,
{ "in" : "query" , "name" : "client_id" , "required" : true , "schema" : { "type" : "string" } } ,
{ "in" : "query" , "name" : "redirect_uri" , "required" : true , "schema" : { "type" : "string" , "format" : "uri" } } ,
{ "in" : "query" , "name" : "state" , "required" : true , "schema" : { "type" : "string" } }
] ,
"responses" : { "302" : { "description" : "Redirect to login or exact client callback" } }
}
} ,
"/connect/token" : {
"post" : {
"tags" : [
"User Action"
] ,
"summary" : "Exchange a one-time authorization code" ,
2026-10-04 12:24:13 +02:00
"description" : "The nested user object carries role, permissions, and avatar_url for the tenant owning the application." ,
2026-10-01 23:15:57 +02:00
"security" : [ { "clientBasic" : [ ] } ] ,
"requestBody" : { "required" : true , "content" : { "application/x-www-form-urlencoded" : { "schema" : { "type" : "object" } } } } ,
"responses" : { "200" : { "description" : "Opaque client user session" } , "401" : { "description" : "Invalid code or client credentials" } }
}
} ,
"/connect/userinfo" : {
"get" : {
"tags" : [
"User Action"
] ,
2026-10-04 12:24:13 +02:00
"summary" : "Validate a client user session and read the caller's role and picture" ,
"description" : "Returns id, email, display_name, avatar_url, tenant and application identity, role, and permissions." ,
2026-10-01 23:15:57 +02:00
"security" : [ { "userBearer" : [ ] } ] ,
"responses" : { "200" : { "description" : "Authenticated client user" } , "401" : { "description" : "Invalid, revoked, or expired session" } }
}
} ,
"/connect/logout" : {
"post" : {
"tags" : [
"User Action"
] ,
"summary" : "Revoke a client user session" ,
"security" : [ { "userBearer" : [ ] } ] ,
"responses" : { "204" : { "description" : "Session revoked" } }
}
} ,
"/api/v1/applications/{id}/client-config" : {
"post" : {
"tags" : [
"Application Management"
] ,
"summary" : "Configure exact redirect URIs and rotate the client secret" ,
"security" : [ { "session" : [ ] } ] ,
"parameters" : [ { "in" : "path" , "name" : "id" , "required" : true , "schema" : { "type" : "string" , "format" : "uuid" } } ] ,
"responses" : { "200" : { "description" : "Configuration saved; client secret returned once" } }
}
} ,
"/api/v1/users" : {
"post" : {
"tags" : [
"User Management"
] ,
2026-10-04 16:57:30 +02:00
"summary" : "Invite a tenant user with a role" ,
"description" : "Requires users.manage in the tenant, which owner, admin, developer, and operator hold. role_id must name a role of that tenant whose permissions are a subset of the caller's own." ,
2026-10-01 23:15:57 +02:00
"security" : [ { "session" : [ ] } ] ,
"responses" : { "201" : { "description" : "User invited" } }
}
} ,
"/api/v1/users/{id}/access" : {
"put" : {
"tags" : [
"User Management"
] ,
"summary" : "Set tenant role and application access" ,
2026-10-04 16:57:30 +02:00
"description" : "Requires users.manage. The caller may not grant a role more powerful than their own, and may not demote the last owner of a tenant." ,
2026-10-01 23:15:57 +02:00
"security" : [ { "session" : [ ] } ] ,
"parameters" : [ { "in" : "path" , "name" : "id" , "required" : true , "schema" : { "type" : "string" , "format" : "uuid" } } ] ,
"responses" : { "204" : { "description" : "Access updated" } }
}
} ,
2026-10-04 12:24:13 +02:00
"/api/v1/roles" : {
"get" : {
2026-10-04 16:57:30 +02:00
"tags" : [ "Role Management" ] ,
"summary" : "List the roles available in a tenant" ,
"description" : "Returns every built-in role plus the roles this tenant defined, each annotated with whether the caller may assign it." ,
"security" : [ { "session" : [ ] } ] ,
"parameters" : [ { "in" : "query" , "name" : "tenant_id" , "required" : true , "schema" : { "type" : "string" , "format" : "uuid" } } ] ,
"responses" : { "200" : { "description" : "Role catalog" , "content" : { "application/json" : { "schema" : { "type" : "object" , "properties" : { "roles" : { "type" : "array" , "items" : { "$ref" : "#/components/schemas/Role" } } } } } } } , "404" : { "description" : "Not a member of that tenant" } }
} ,
"post" : {
"tags" : [ "Role Management" ] ,
"summary" : "Define a role for a tenant" ,
"description" : "Requires roles.manage in the tenant. The role may not grant a permission the caller does not already hold, so an administrator cannot mint their way above their own authority." ,
"security" : [ { "session" : [ ] } ] ,
"requestBody" : { "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/RoleRequest" } } } } ,
"responses" : { "201" : { "description" : "Role created" , "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/Role" } } } } , "400" : { "description" : "Malformed slug or name, or an unknown permission" } , "404" : { "description" : "The caller does not hold roles.manage, or the role would exceed their authority" } , "409" : { "description" : "The slug is already used in this tenant" } }
}
} ,
"/api/v1/roles/{id}" : {
"put" : {
"tags" : [ "Role Management" ] ,
"summary" : "Rename a role and replace its permissions" ,
"description" : "Built-in roles are immutable. Saving replaces the permissions of every member holding the role, so treat it as a live change." ,
"security" : [ { "session" : [ ] } ] ,
"parameters" : [ { "in" : "path" , "name" : "id" , "required" : true , "schema" : { "type" : "string" , "format" : "uuid" } } ] ,
"requestBody" : { "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/RoleRequest" } } } } ,
"responses" : { "200" : { "description" : "Role updated" , "content" : { "application/json" : { "schema" : { "$ref" : "#/components/schemas/Role" } } } } , "400" : { "description" : "Malformed name or an unknown permission" } , "404" : { "description" : "Unknown role, a built-in role, or a role belonging to another tenant" } }
} ,
"delete" : {
"tags" : [ "Role Management" ] ,
"summary" : "Delete one of the tenant's own roles" ,
"security" : [ { "session" : [ ] } ] ,
"parameters" : [ { "in" : "path" , "name" : "id" , "required" : true , "schema" : { "type" : "string" , "format" : "uuid" } } ] ,
"responses" : { "204" : { "description" : "Role deleted" } , "404" : { "description" : "Unknown role, a built-in role, or a role belonging to another tenant" } , "409" : { "description" : "The role still has members" } }
}
} ,
"/roles" : {
"get" : {
"tags" : [ "Role Management" ] ,
"summary" : "Role management screen" ,
2026-10-04 12:24:13 +02:00
"security" : [ { "session" : [ ] } ] ,
2026-10-04 16:57:30 +02:00
"responses" : { "200" : { "description" : "HTML page" } }
2026-10-04 12:24:13 +02:00
}
} ,
2026-10-01 23:15:57 +02:00
"/api/v1/users/{id}/approve" : {
"post" : {
"tags" : [ "User Management" ] ,
"summary" : "Approve a waiting-room user into a tenant" ,
2026-10-04 16:57:30 +02:00
"description" : "Requires a system administrator session. Unlike the tenant-scoped endpoints this is not limited by the caller's own role, because it grants a first membership across tenants." ,
2026-10-01 23:15:57 +02:00
"security" : [ { "session" : [ ] } ] ,
"parameters" : [ { "in" : "path" , "name" : "id" , "required" : true , "schema" : { "type" : "string" , "format" : "uuid" } } ] ,
"responses" : { "204" : { "description" : "User approved" } , "404" : { "description" : "User or permission not found" } }
}
}
}
}