Download OpenAPI specification:
The gateway-owned "platform" surface: every /api/* route mounted directly by internal/gateway itself (foundation_routes.go, platform_routes.go, admin_position_routes.go, admin_group_routes.go), as opposed to a module's own /api/<moduleKey>/* fragment. This is not a module: there is no module.manifest.json/ authz.fragment.json for the platform; it is documented here because the module contract's binding rule ("the running service must enforce this contract at the HTTP boundary") applies equally to the platform's own consumer-facing surface, so a developer can use Kiban from docs alone.
Every route here is a fixed reverse proxy (never the dynamic, opaque module-key proxy in proxy.go): the gateway rewrites the outbound path to an internal /internal/... route on one of the four foundation services (authz, org, registry) and, for the self-scoped reads, forces the outbound kcSub to the validated bearer's own subject (never a client-supplied value: see foundation_routes.go's own doc comment). All routes require a valid bearer (RequireAuth); routes under .../admin/... additionally require the superadmin guard (RequireSuperadmin): a fail-closed check: if authz itself is unreachable, the admin guard returns 503 AUTHORIZATION_UNAVAILABLE, never a silent allow.
Validated the same way a module's own fragment is (the internal/testopenapi helper, github.com/getkin/kin-openapi): internal/gateway/platform_openapi_live_test.go drives real requests through a live kiban-test gateway and validates the recorded response against this fragment's declared schema: at least the happy path and one error per route, so this document is trustworthy, not aspirational.
Bearer-gated fixed proxy to authz's own /internal/authz/effective-access/can (internal/authz/http.go). authz itself rejects a body naming a subject other than the bearer (400), and a global-scope request without requiredPlatformRole (400 VALIDATION_ERROR), the gateway adds nothing beyond bearer validation at the edge.
| featureKey required | string |
| moduleKey | string |
| scope required | string Enum: "global" "company" |
| companyId | string <uuid> |
| requiredPlatformRole | string |
| allowPlatformOperatorCompanyScope | boolean |
| requiredCompanyRole | string |
object (EffectiveAccessObjectRef) | |
| relation | string |
| requiresEligibility | boolean |
| correlationId | string |
{- "featureKey": "string",
- "moduleKey": "string",
- "scope": "global",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "requiredPlatformRole": "string",
- "allowPlatformOperatorCompanyScope": true,
- "requiredCompanyRole": "string",
- "object": {
- "type": "string",
- "id": "string"
}, - "relation": "string",
- "requiresEligibility": true,
- "correlationId": "string"
}{- "data": {
- "allowed": true,
- "reason": "string",
- "evidence": [
- {
- "key": "string",
- "value": "string"
}
]
}
}Same subject and scope rules as can, bearer-gated fixed proxy to authz's own /internal/authz/effective-access/batch-can. At most 100 items per request (400 VALIDATION_ERROR above); a request with denied items writes one audit event listing them (the first 50, with a truncated flag).
required | Array of objects <= 100 items |
{- "items": [
- {
- "object": {
- "type": "string",
- "id": "string"
}, - "relation": "string"
}
]
}{- "data": [
- {
- "object": {
- "type": "string",
- "id": "string"
}, - "relation": "string",
- "decision": {
- "allowed": true,
- "reason": "string",
- "evidence": [
- {
- "key": "string",
- "value": "string"
}
]
}
}
]
}Self-scoped, internal-network-unauthenticated downstream (authz's own /internal/authz/effective-access/summary, kcSub as a plain query param), the gateway is the bearer-validating boundary and forces kcSub to the validated bearer's own subject, overwriting any client-supplied value. objectAccess is direct-tuples-only (never a userset-closure expansion); a position/group-held tier never appears here even when it is real and enforced elsewhere.
| companyId | string <uuid> |
{- "data": {
- "apiVersion": 0,
- "subjectId": "string",
- "companyId": "string",
- "moduleKey": "string",
- "featureKeys": [
- "string"
], - "roleBindings": [
- {
- "scope": "global",
- "role": "string",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda"
}
], - "objectAccess": [
- {
- "objectType": "string",
- "objectId": "string",
- "relations": [
- "string"
]
}
], - "rowScopes": [
- null
], - "fieldPolicies": [
- null
]
}
}Self-scoped fixed proxy to org's own /internal/org/me/companies (internal/org/http.go's handleMeCompanies), kcSub forced to the bearer's own subject.
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "code": "string",
- "name": "string",
- "isActive": true
}
]
}Bearer-gated fixed proxy to org's own /internal/org/companies/{companyId}/members, kcSub forced to the bearer's own subject. org's own handler enforces active-membership-or-superadmin (requireMemberOrAdmin), a non-member caller gets 403, not a 404 (never leaks whether the company exists to an outsider vs. a non-member).
| companyId required | string <uuid> |
| q | string |
| page | integer >= 1 Default: 1 |
| pageSize | integer [ 1 .. 100 ] Default: 25 |
{- "data": {
- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "displayName": "string",
- "email": "string",
- "hasLinkedUser": true
}
], - "total": 0,
- "page": 0,
- "pageSize": 0,
- "totalPages": 0
}
}Fixed proxy to org's /internal/org/companies/{companyId}/members/by-kcsub/{subject}. A registered app's backend (client-credentials token whose azp is the app's service client id) or a superadmin asks whether subject is an active member of the company and which member record that is. Never a directory: one subject, one answer.
| companyId required | string <uuid> |
| subject required | string |
{- "data": {
- "isMember": true,
- "isActive": true,
- "memberId": "92983ab9-49c8-444b-85ae-6e40402cf72e"
}
}Fixed proxy to authz's own /internal/authz/grants (internal/authz/http.go), the one write path exposed through the gateway for raw tuple grants. A superadmin may call it; so may a registered app's backend (a client-credentials token whose azp is the app's service client id), which authz then restricts to tuples on that app's own object types.
| op required | string Enum: "grant" "revoke" |
| companyId required | string <uuid> |
| correlationId | string |
required | Array of objects |
{- "op": "grant",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "correlationId": "string",
- "tuples": [
- {
- "objectType": "string",
- "objectId": "string",
- "relation": "string",
- "subjectType": "string",
- "subjectId": "string",
- "subjectRelation": "string"
}
]
}{- "data": {
- "status": "string",
- "count": 0
}
}Superadmin-gated fixed proxy to authz's own POST /internal/authz/platform-roles. The platform role's one record is the authz tuple system:platform#superadmin @ user:<subjectId>; authz writes it with its grant-ledger and audit rows in one transaction. The subject must already exist in identity (404 otherwise); kiban-superadmin is the only role (422 otherwise).
| subjectId required | string The subject's Keycloak subject id (kcSub). |
| role required | string Value: "kiban-superadmin" |
{- "subjectId": "string",
- "role": "kiban-superadmin"
}{- "data": {
- "subjectId": "string",
- "role": "kiban-superadmin"
}
}Superadmin-gated fixed proxy to authz's own DELETE /internal/authz/platform-roles/{role}/{subjectId}. 404 when the subject does not hold the role; 409 CONFLICT when it is the platform's last superadmin (the caller revoking themselves included).
| role required | string Value: "kiban-superadmin" |
| subjectId required | string The subject's Keycloak subject id (kcSub). |
{- "data": {
- "subjectId": "string",
- "role": "kiban-superadmin"
}
}Bearer-gated only (deliberately outside the superadmin gate, avoids a gateway<->authz cycle, platform_routes.go's own header comment). Fixed proxy to the registry service, path forwarded unchanged.
{- "data": [
- {
- "module": "string",
- "installed": true,
- "enabled": true,
- "dependencies": [
- "string"
], - "missingDependencies": [
- "string"
]
}
]
}{- "data": [
- {
- "moduleKey": "string",
- "displayName": "string",
- "scopeType": "string",
- "mandatory": true,
- "basePath": "string",
- "healthPath": "string",
- "port": 0,
- "licenseClass": "string",
- "manifestVersion": "string",
- "isActive": true
}
]
}Superadmin-gated fixed proxy to the registry's /internal/platform/apps. Registers (or re-registers) an app that runs beside Kiban: its object types and relations (the authorization fragment), its feature keys and the service client whose token identifies its backend. The app is installed at once and enabled per company with the enable route. No restart; the gateway never proxies to it.
| key required | string^[a-z][a-z0-9_]{1,31}$ |
| displayName required | string |
| version required | string |
| serviceClientId required | string^[a-z][a-z0-9_-]{1,63}$ |
| features | Array of strings |
required | object The fragment's "relations" section, one entry per object type |
{- "key": "string",
- "displayName": "string",
- "version": "string",
- "serviceClientId": "string",
- "features": [
- "string"
], - "authzFragment": { }
}{- "data": {
- "module": "string",
- "installed": true,
- "enabled": true,
- "dependencies": [
- "string"
], - "missingDependencies": [
- "string"
]
}
}The only metrics surface exposed through the gateway, behind the superadmin guard, and the single scrape target for the whole platform: the gateway's own registry merged with the /metrics of registry, identity, org, authz and every installed+enabled module, fetched concurrently on the internal network (2 s per target). Every series carries a service="<name>" label; metric families of the same name from several services share one # HELP/# TYPE. A target that fails or times out contributes only kiban_metrics_scrape_up{service="<name>"} 0 (1 on success) — the response is always 200, never a failed scrape. Plain-text Prometheus exposition format (text/plain; version=0.0.4), not JSON, documented here with no schema.
{- "error": {
- "code": "string",
- "message": "string",
- "details": null
}
}Superadmin-gated fixed proxy to org's /internal/org/units. A company is a unit with typeKey "company" and no parent; creating an active company also writes every enabled module's administrator relation for it in the same transaction. Any other unit names its parent. Codes are normalised to upper case.
| typeKey required | string "company" for a company; a taxonomy key such as "business_unit" below it |
| parentId | string or null <uuid> |
| code required | string |
| name required | string |
{- "typeKey": "string",
- "parentId": "70850378-7d3c-4f45-91b7-942d4dfbbd43",
- "code": "string",
- "name": "string"
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "typeKey": "string",
- "parentId": "70850378-7d3c-4f45-91b7-942d4dfbbd43",
- "code": "string",
- "name": "string",
- "isActive": true
}
}| id required | string <uuid> |
{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "typeKey": "string",
- "parentId": "70850378-7d3c-4f45-91b7-942d4dfbbd43",
- "code": "string",
- "name": "string",
- "isActive": true
}
}| id required | string <uuid> |
{- "data": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "typeKey": "string",
- "parentId": "70850378-7d3c-4f45-91b7-942d4dfbbd43",
- "code": "string",
- "name": "string",
- "isActive": true
}
]
}Superadmin-gated fixed proxy to org's /internal/org/members. The member has no login until it is linked to a subject with the link-user route.
| companyId required | string <uuid> |
| code required | string |
| displayName required | string |
string |
{- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "code": "string",
- "displayName": "string",
- "email": "string"
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "code": "string",
- "displayName": "string",
- "email": "string",
- "userId": "string",
- "isActive": true
}
}| companyId required | string <uuid> |
| page | integer >= 1 Default: 1 |
| pageSize | integer [ 1 .. 100 ] Default: 25 |
{- "data": {
- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "code": "string",
- "displayName": "string",
- "email": "string",
- "userId": "string",
- "isActive": true
}
], - "total": 0,
- "page": 0,
- "pageSize": 0,
- "totalPages": 0
}
}| id required | string <uuid> |
| displayName | string |
string | |
| isActive | boolean |
{- "displayName": "string",
- "email": "string",
- "isActive": true
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "code": "string",
- "displayName": "string",
- "email": "string",
- "userId": "string",
- "isActive": true
}
}kcSub is the subject's sub claim; the identity record exists once that subject has made one request through the gateway. A subject is linked to at most one member per company.
| id required | string <uuid> |
| kcSub required | string |
{- "kcSub": "string"
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "code": "string",
- "displayName": "string",
- "email": "string",
- "userId": "string",
- "isActive": true
}
}| id required | string <uuid> |
{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "code": "string",
- "displayName": "string",
- "email": "string",
- "userId": "string",
- "isActive": true
}
}| companyId required | string <uuid> |
| page | integer >= 1 Default: 1 |
| pageSize | integer [ 1 .. 100 ] Default: 25 |
{- "data": {
- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "code": "string",
- "title": "string",
- "orgUnitId": "d6d5d992-9f70-470c-bee4-0571454bbbd7",
- "assignmentId": "dbf5135c-beb7-4185-80db-2e980c77381b",
- "memberId": "92983ab9-49c8-444b-85ae-6e40402cf72e",
- "holderDisplayName": "string"
}
], - "total": 0,
- "page": 0,
- "pageSize": 0,
- "totalPages": 0
}
}| companyId required | string <uuid> |
| code required | string |
| title required | string |
| orgUnitId required | string <uuid> |
{- "code": "string",
- "title": "string",
- "orgUnitId": "d6d5d992-9f70-470c-bee4-0571454bbbd7"
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "code": "string",
- "title": "string",
- "orgUnitId": "d6d5d992-9f70-470c-bee4-0571454bbbd7"
}
}| id required | string <uuid> |
| memberId required | string <uuid> |
{- "memberId": "92983ab9-49c8-444b-85ae-6e40402cf72e"
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "positionId": "da3402dc-13f8-45f9-83a6-bde06dd8eb35",
- "memberId": "92983ab9-49c8-444b-85ae-6e40402cf72e",
- "validFrom": "2019-08-24",
- "validTo": "2019-08-24"
}
}| id required | string <uuid> |
{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "positionId": "da3402dc-13f8-45f9-83a6-bde06dd8eb35",
- "memberId": "92983ab9-49c8-444b-85ae-6e40402cf72e",
- "validFrom": "2019-08-24",
- "validTo": "2019-08-24"
}
}| companyId required | string <uuid> |
| page | integer >= 1 Default: 1 |
| pageSize | integer [ 1 .. 100 ] Default: 25 |
{- "data": {
- "items": [
- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "code": "string",
- "name": "string",
- "source": "string",
- "externalRef": "string",
- "isActive": true,
- "isKibanManaged": true,
- "memberCount": 0
}
], - "total": 0,
- "page": 0,
- "pageSize": 0,
- "totalPages": 0
}
}| companyId required | string <uuid> |
| code required | string |
| name required | string |
{- "code": "string",
- "name": "string"
}{- "data": {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "companyId": "8bb73d03-06b4-47c7-80c7-59301f770eda",
- "code": "string",
- "name": "string",
- "source": "string",
- "externalRef": "string",
- "isActive": true,
- "isKibanManaged": true
}
}| id required | string <uuid> |
{- "data": [
- {
- "groupId": "eb54e96e-21b8-4f54-9cd4-80fccbd06f55",
- "memberId": "92983ab9-49c8-444b-85ae-6e40402cf72e",
- "addedBy": "string",
- "addedAt": "2019-08-24T14:15:22Z",
- "memberDisplayName": "string",
- "memberEmail": "string"
}
]
}| id required | string <uuid> |
| memberId required | string <uuid> |
{- "memberId": "92983ab9-49c8-444b-85ae-6e40402cf72e"
}{- "data": {
- "groupId": "eb54e96e-21b8-4f54-9cd4-80fccbd06f55",
- "memberId": "92983ab9-49c8-444b-85ae-6e40402cf72e",
- "addedBy": "string",
- "addedAt": "2019-08-24T14:15:22Z",
- "memberDisplayName": "string",
- "memberEmail": "string"
}
}| id required | string <uuid> |
| memberId required | string <uuid> |
{- "data": {
- "groupId": "eb54e96e-21b8-4f54-9cd4-80fccbd06f55",
- "memberId": "92983ab9-49c8-444b-85ae-6e40402cf72e"
}
}