CTEM
CTEM-API v2.4

SDK

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>.

Session

tag session

GET /v1/session

Permission: bearer principal. No extra permission decorator on the controller.

Request

GET /v1/session
Authorization: Bearer <token>

Response

Confirmed in the controller: userId, orgId, role, permissions, serviceAccount.

Full schema: gateway /docs.

Scans

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.

Request

POST /v1/scans
Authorization: Bearer <token>

{
  "scannerType": "sca",
  "assetSelector": {}
}

Response

Partial. Confirmed fields on a scan include:

status

  • queued
  • running
  • succeeded
  • partial
  • failed
  • cancelled

conclusion

  • pending
  • passed
  • failed

deployConclusion

  • pending
  • allowed
  • blocked

Full object: gateway /docs. No sample score or finding list.

Full schema: gateway /docs.

Assets

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.

Request

POST /v1/assets
Authorization: Bearer <token>

{
  "kind": "domain",
  "externalKey": "example.com",
  "name": "example.com",
  "source": "asm"
}

Response

Unknown beyond “returns the asset”. Schema: gateway /docs.

Kinds confirmed in the contract:

  • repository
  • package
  • container_image
  • cloud_resource
  • kubernetes_workload
  • host
  • domain
  • ip_range
  • web_application
  • api_endpoint
  • saas_app
  • iac_stack

Full schema: gateway /docs.

Findings

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.

Request

PATCH /v1/findings/:id/triage
Authorization: Bearer <token>

{
  "state": "triaged",
  "reason": "Owner assigned"
}

Response

Unknown (proxied). Schema: gateway /docs.

Full schema: gateway /docs.

Policies

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.

Request

POST /v1/policies
Authorization: Bearer <token>

{
  "name": "Block high risk deploys",
  "condition": { "minRiskScore": 70 },
  "actions": ["block_deploy"]
}

Response

Unknown (proxied). Schema: gateway /docs.

Full schema: gateway /docs.

Members

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.

Request

POST /v1/org/members
Authorization: Bearer <token>

{
  "email": "teammate@example.com",
  "role": "developer"
}

Response

Unknown (proxied). Schema: gateway /docs.

Full schema: gateway /docs.

Meters

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.

Request

GET /v1/meters/scan-kicks?source=ci&limit=50
Authorization: Bearer <token>

Response

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.

Not on this page

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.