Searching the Inventory Through the API
How to get material out of the crypto inventory programmatically: what the query surfaces are, what each one is good for, and how to filter and page through results.
There is no free-form query endpoint
Start here, because it is the question most integrations ask first. CBOM Secure exposes no generic "send a query, get documents" endpoint. There is no path that takes a database expression, and /query, /objects and /discovery — URL shapes reserved during design — were never implemented and have been withdrawn from the product. Do not build against them.
This is deliberate rather than missing. An endpoint that forwards an arbitrary expression to the database makes every field a public interface, and a query that performs acceptably on a test estate can take a production one down. What exists instead are four purpose-built surfaces, each with its own filters, its own paging and its own bounded cost.
| Surface | Use it to |
|---|---|
| Export | Pull material in bulk, as a standard document another tool can read |
| Widget data | Ask the same question a dashboard tile asks, with your own parameters |
| Analysis | Get a risk or policy rollup without computing it yourself |
| History | Find what changed, and when |
Export — bulk material
The CycloneDX export endpoints are the right surface for an integration that wants the inventory itself: feeding a SIEM, a GRC platform, an asset database, or an offline report.
| Endpoint | Returns |
|---|---|
GET /cyclonedx/export/certificates | Certificates |
GET /cyclonedx/export/keys | Keys |
GET /cyclonedx/export/protocols | Protocol endpoints |
GET /cyclonedx/export/binary | Binaries |
GET /cyclonedx/export/sourcecode | Source-code findings |
GET /cyclonedx/export | Every type, time-bounded |
Filters are query-string parameters, and a parameter that accepts a list takes comma-separated values matching any of them. Every endpoint accepts discoverTimeFrom, discoverTimeTo and scanHost; each type adds its own. The full filter tables and the field mapping are on the CycloneDX Export Specification page.
Certificates expiring in a window, as a worked request:
GET /cbomsrv/api/v1/cyclonedx/export/certificates
?discoverTimeFrom=2026-01-01T00:00:00Z
&algorithm=RSA,ECDSA
Authorization: Bearer <session_id>
The combined /cyclonedx/export requires a time bound. At least one of discoverTimeFrom or discoverTimeTo must be present, or the request is rejected with a 400. The bound applies to discovery time or scan time, so material first discovered before the window is still included when a scan inside the window saw it — which is what makes an incremental sync correct rather than merely cheap.
Syncing incrementally. Keep the timestamp of your last successful pull and pass it as discoverTimeFrom on the next. Use the per-type endpoints rather than the combined one: a sync that only cares about certificates should not pay for binaries.
Widget data — any dashboard figure
Every number on every dashboard is available over the API, under parameters you choose. This is the surface for "give me the same breakdown the console shows, but for my own scope".
The pattern is always two calls.
GET /cbomsrv/api/v1/ui/dashboard/widgets
GET /cbomsrv/api/v1/ui/dashboard/widgets/{wid}
POST /cbomsrv/api/v1/ui/dashboard/widgets/{wid}/data
The first lists the catalogue. The second returns one widget's definition including its query schema — the fields that widget's data call accepts. The third posts parameters matching that schema and returns the data.
Read the schema rather than guessing at it. Parameter validation rejects an unknown field outright, so a request carrying a field the widget does not declare fails before the handler runs. The schema is also the authoritative list for that deployment: widget definitions live in the database, so a deployment can carry widgets another does not.
POST /cbomsrv/api/v1/ui/dashboard/widgets/Cert-TBL/data
Content-Type: application/json
{
"pageNo": 1,
"pageSize": 50,
"tagScope": "(\"Production\" $or \"Sales\") $not \"Test\""
}
Table widgets page. Chart widgets return groups, and each group carries the request body that would open the matching table — so a chart result tells you how to drill into it rather than requiring you to construct the follow-up yourself. Replay the emitted body against the named endpoint and the row count matches the number in the chart.
Scoping by tag
tagScope is the filter that most integrations actually want, because it is the one that maps to how an organisation thinks about its estate. It takes a boolean expression over tag names and is accepted by the widget data endpoints and the analysis endpoints.
Tag names are always quoted, and operators carry a $:
"Production"
("Production" $or "Sales") $not "Test"
("Production" $or "Sales") $and $not "Test"
("Production" $or "Sales") $except "Test"
Quoting is mandatory because real tag names contain spaces, and once it is mandatory a tag name can never collide with an operator — so the bare forms and, or and not are accepted too. $not is unary in prefix position and means "and not" after a complete operand; position alone decides. Precedence is $not, then $and, then $or, and parentheses override.
A named scope defined once in the console can be referenced by name with @"Scope Name" instead of being spelled out, and GET /cmtags/scopes/ lists them. POST /cmtags/scopes/validate checks an expression without running it, which is worth calling once when your integration starts rather than discovering a typo as an empty result.
An empty result and a wrong expression look the same from the outside. Validate the expression, and treat zero rows as a result to investigate rather than as "no material matches". A tag name that does not exist is a perfectly valid expression that matches nothing.
Analysis — risk and policy rollups
| Endpoint | Returns |
|---|---|
GET /analytic/host-analysis/ | Per-host risk rollup |
POST /analytic/host-analysis-filtered/ | The same, under a filter |
GET /analytic/enterprise-analysis/ | Estate-wide rollup |
These return computed positions rather than documents. Use them when the question is "how bad is it" rather than "what is there" — they are far cheaper than exporting the inventory and computing the answer yourself, and they apply the same scoring the dashboards use, so the numbers agree. The scoring rules are on the Risk and PQC Scoring page.
Policy results come from the policy widgets rather than a separate endpoint; GET /policy/ lists the policies and GET /policy/standard-policy the built-in baselines.
History — what changed
| Endpoint | Returns |
|---|---|
GET /cmhistory/ | The change feed across the whole inventory |
GET /cmhistory/{canonicalId} | One material's timeline |
GET /cmhistory/{canonicalId}/at | That material's state at a point in time |
GET /cmhistory/{canonicalId}/diff | Field-level change between two versions |
This is the surface for an integration that reacts to change rather than re-reading state: poll the feed, act on what is new. A bare date on the ts parameter covers the whole of that day.
History is readable and never writable. No path in the API can modify or delete a history entry, which is what makes it usable as evidence.
Paging, ordering and limits
Table widget data takes pageNo and pageSize and returns a total count alongside the rows, so a client can size its loop before starting it. The export endpoints stream a whole filtered set rather than paging — narrow with filters, not with offsets.
Request size and rate limits are on the API Authentication page. The practical guidance: filter server-side, pull incrementally by time, and prefer a per-type export over the combined one.
Choosing a surface
| If you want | Use |
|---|---|
| The whole inventory in a standard format | Per-type CycloneDX export, time-bounded, pulled incrementally |
| One figure, scoped your way | The widget's definition, then its /data path |
| A risk position | The analysis endpoints |
| Only what changed since last time | The history feed, or an export with discoverTimeFrom |
| To be told rather than to poll | A notification rule — see Running a Scan Through the API |
Common problems
422 on a widget data call. The request carries a field the widget's schema does not declare. Unknown fields are rejected rather than ignored. Fetch the definition and send only what it declares.
A tagScope returns nothing. Either the expression does not parse, or it parses and matches nothing. POST /cmtags/scopes/validate separates the two. A tag created after your integration last cached the tag list is resolved on a retry, so a transient empty result on a brand-new tag is expected.
400 on /cyclonedx/export. No time bound. Supply discoverTimeFrom, discoverTimeTo, or both — or use a per-type endpoint.
An export times out. The filter is too broad for the estate. Narrow by type first, then by time window, then by scanning host.
Chart and table disagree. They should not — each chart group carries the exact request body that opens its table. Rebuilding that body by hand is the usual cause of a mismatch; replay the emitted one.
404 on /query or /objects. Those paths were never implemented and have been withdrawn. Use the surfaces on this page.