GET /v1/session
Permission: bearer principal. No extra permission decorator on the controller.
This is the CTEM gateway HTTP API. It is not a language SDK.
Org comes from the bearer token (JWT or PAT). The client does not send an org id.
Unauthenticated calls to these routes are 401 when the route exists. Live request and
response schemas: the gateway’s /docs. Examples below are shapes from the
current controllers and contracts, not captured fixtures.
Auth header on every example: Authorization: Bearer <token>.
tag session
GET /v1/session
Permission: bearer principal. No extra permission decorator on the controller.
GET /v1/session
Authorization: Bearer <token>
Confirmed in the controller: userId, orgId,
role, permissions, serviceAccount.
Full schema: gateway /docs.
tag scans
Read scan:read. Kick scan:run. Optional
Idempotency-Key on POST (forwarded; the gateway does not emit the meter
row).
GET /v1/scans
Read scan:read.
GET /v1/scans/:id
Read scan:read. CI and deploy tooling poll this.
conclusion is fail-build. deployConclusion is
block-deploy. The client cannot POST either field.
POST /v1/scans
Kick scan:run. Body from CreateScanRequest:
scannerType (sca | sast | container | iac | secrets | asm | cloud_posture),
optional assetSelector (assetIds uuids, kinds,
tags), options object (default {}). Optional
externalId for kick idempotency. Do not send conclusion or
deployConclusion.
POST /v1/scans/sbom
Kick scan:run. IngestSbomRequest:
assetExternalKey, format
(cyclonedx-json | spdx-json | syft-json), exactly one of
artifactKey or document, optional ref,
commitSha, optional externalId.
POST /v1/scans
Authorization: Bearer <token>
{
"scannerType": "sca",
"assetSelector": {}
}
Partial. Confirmed fields on a scan include:
status
conclusion
deployConclusion
Full object: gateway /docs. No sample score or finding list.
Full schema: gateway /docs.
tag assets
Read asset:read. Write asset:write. Discover
integration:manage.
GET /v1/assets
Read asset:read. Query from ListAssetsQuery: optional
kind, exposure, criticality,
ownerTeam, q, cursor, limit
(default 50, max 200).
GET /v1/assets/:id
Read asset:read.
GET /v1/assets/:id/graph
Read asset:read. Query depth (controller default
"2").
POST /v1/assets/discover
Discover integration:manage. No body in the controller.
POST /v1/assets
Write asset:write. UpsertAssetRequest:
kind, externalKey, name, source,
plus optional exposure, criticality, data classes, owner, tags, attributes.
POST /v1/assets
Authorization: Bearer <token>
{
"kind": "domain",
"externalKey": "example.com",
"name": "example.com",
"source": "asm"
}
Unknown beyond “returns the asset”. Schema: gateway /docs.
Kinds confirmed in the contract:
repositorypackagecontainer_imagecloud_resourcekubernetes_workloadhostdomainip_rangeweb_applicationapi_endpointsaas_appiac_stackFull schema: gateway /docs.
tag findings
Read finding:read. Triage finding:triage.
GET /v1/findings
Read finding:read. Query from ListFindingsQuery:
assetId, scannerType, severity,
state, validation, minRiskScore,
fixAvailable, slaBreached, q,
cursor, limit.
GET /v1/findings/:id
Read finding:read.
GET /v1/findings/:id/risk
Read finding:read. Why the score is what it is.
PATCH /v1/findings/:id/triage
Triage finding:triage. TriageFindingRequest:
state
(open | triaged | in_progress | resolved | risk_accepted | false_positive | suppressed),
reason (1–2000 chars), optional expiresAt. When state is
risk_accepted, expiresAt is required by the policy
service. That extra rule is enforced off this controller, not as a gateway check.
PATCH /v1/findings/:id/triage
Authorization: Bearer <token>
{
"state": "triaged",
"reason": "Owner assigned"
}
Unknown (proxied). Schema: gateway /docs.
Full schema: gateway /docs.
tag policies
Read policy:read. Write policy:write. Org comes from the token.
GET /v1/policies
Read policy:read.
GET /v1/policies/:id
Read policy:read.
POST /v1/policies
Write policy:write. CreatePolicyRequest:
name, condition, actions (non-empty subset of
notify | ticket | fail_build | block_deploy; ignore is not
an editor action), optional description, enabled,
priority, slaHours. Tenant webhook / Jira URL fields are
refused.
PATCH /v1/policies/:id
Write policy:write. Partial of the same request.
POST /v1/policies
Authorization: Bearer <token>
{
"name": "Block high risk deploys",
"condition": { "minRiskScore": 70 },
"actions": ["block_deploy"]
}
Unknown (proxied). Schema: gateway /docs.
Full schema: gateway /docs.
tag members
List org:read. Invite, role, and disable member:manage.
GET /v1/org/members
List org:read.
POST /v1/org/members
Invite member:manage. Body { "email", "role" } with role
owner | admin | security_analyst | developer | auditor.
PATCH /v1/org/members/:userId/role
Role member:manage. Body { "role" }.
DELETE /v1/org/members/:userId
Disable member:manage. Disables membership. Not a Keycloak Admin call.
POST /v1/org/members
Authorization: Bearer <token>
{
"email": "teammate@example.com",
"role": "developer"
}
Unknown (proxied). Schema: gateway /docs.
Full schema: gateway /docs.
tag meters
scan:read. Read-only. Query orgId is dropped. This route does
not emit a meter row.
GET /v1/meters/scan-kicks
Read scan:read. Optional from, to (ISO-8601;
from inclusive, to exclusive), source
(manual | api | webhook | ci | schedule), limit (default
50, cap 200), cursor.
GET /v1/meters/scan-kicks?source=ci&limit=50
Authorization: Bearer <token>
Unknown (list shape not read from the controller). Schema: gateway
/docs. Summary confirmed: scan.kick usage for the token’s organization.
Full schema: gateway /docs.
No public unauthenticated route was in the gateway route controllers. Health, OIDC, and login are not documented. Browser sign-in stays in the product app (Keycloak). This site does not document a login API.
Anything not listed above is unknown. Scanners, connectors, and internal paths are not added here.