Filtering & sorting
Collection endpoints narrow their results with query parameters. There is no cross-cutting
query language and no generic sort parameter — each endpoint declares the filters that make
sense for its resource, and the interactive reference is the
authoritative list of which ones it accepts.
Unknown parameters are ignored, not rejected
Sending a filter an endpoint does not support does not return an error. The parameter is discarded and you receive the unfiltered collection. If a filter appears to have no effect, check its spelling and its presence in the reference before assuming the data is wrong.
How filtering works
| Property | Behaviour |
|---|---|
| Location | Query string. Filters are never sent in a body, including on POST action endpoints. |
| Naming | camelCase, matching the response field they filter on where one exists. |
| Optionality | Every filter is optional. Omitting all of them returns the collection unfiltered. |
| Combination | Multiple filters combine with AND. There is no OR across different parameters. |
| Empty result | A filter that matches nothing returns 200 with an empty items array — not 404. |
| Invalid value | A value that cannot be parsed, or that is outside an accepted enum, returns 400. |
| Case | Enum-valued filters are matched against the documented PascalCase values. Free-text search is case-insensitive. |
Filter categories
The parameters differ per endpoint, but they fall into a small number of recognisable shapes. The examples below are real parameters on the endpoints named; treat them as the pattern rather than as a universal vocabulary.
Enumerated attribute filters
Narrow to one member of a closed set. The accepted values are the enum members listed in the reference for that parameter.
# Packages in a single ecosystem
curl "https://api.ospulse.app/api/v1/packages?ecosystem=Npm&take=50" \
-H "Authorization: Bearer $OSPULSE_TOKEN"| Endpoint | Parameter | Filters by |
|---|---|---|
GET /api/v1/packages | ecosystem | Package ecosystem — npm, NuGet, PyPI, Maven, Go, Cargo, RubyGems, Composer, Docker. |
GET /api/v1/repositories | businessCriticality | The criticality tier recorded against the repository. |
GET /api/v1/repositories | lifecycleStatus | Where the repository sits in its lifecycle. |
GET /api/v1/cra/findings | class | CRA classification of the finding. |
GET /api/v1/repositories/{repositoryId}/quantum/certificates | classification | Post-quantum classification of the certificate. |
GET /api/v1/repositories/{repositoryId}/quantum/certificates | provenance | How the certificate was observed or supplied. |
Enum values move forward
New enum members are added without a breaking-change bump — a new ecosystem, a new lifecycle state. Do not hard-code an exhaustive list of accepted values in a client; read them from the spec, or treat an unrecognised value as pass-through. See Conventions.
Status and workflow filters
Most resources with a workflow expose the current state as a filter, which is how you build a work queue.
| Endpoint | Parameter | Typical use |
|---|---|---|
GET /api/v1/scans | state | Show only running or only failed scans. |
GET /api/v1/cra/findings | state | Separate findings awaiting action from those already acknowledged. |
GET /api/v1/repositories/{repositoryId}/quantum/findings | status | Filter findings by their triage state. |
GET /api/v1/quantum/tls-observations | state | Filter observations by their assessment state. |
# Scans currently in flight for one repository
curl "https://api.ospulse.app/api/v1/scans?repositoryId=3f8a1c27-9c1b-4e2a-8f6d-2a7c5e9b0d14&state=Running" \
-H "Authorization: Bearer $OSPULSE_TOKEN"Severity filters
Where a resource carries a severity, it can usually be filtered on directly — the fastest way to build an alerting integration that only reacts to what matters.
curl "https://api.ospulse.app/api/v1/repositories/$REPO_ID/quantum/findings?severity=Critical&take=50" \
-H "Authorization: Bearer $OSPULSE_TOKEN"Severity is a filter, not an ordering: passing severity=Critical returns only critical rows
in the endpoint's default order, it does not sort a mixed set with critical first.
Relationship filters
Scope a collection to a parent resource by passing its identifier.
| Endpoint | Parameter | Scopes to |
|---|---|---|
GET /api/v1/scans | repositoryId | One repository's scans. |
GET /api/v1/repositories | workspaceId, teamId | One workspace or team. |
GET /api/v1/cra/findings | productId | One CRA product. |
GET /api/v1/cra/duties, GET /api/v1/cra/annex-i | productId | One product's duties or Annex I items. |
GET /api/v1/repositories/{repositoryId}/quantum/findings | scanId | Findings from a specific scan run. |
Note the two ways a relationship is expressed. Where a resource belongs unambiguously to a
parent it appears as a path segment
(/api/v1/repositories/{repositoryId}/quantum/findings); where it can be listed globally and
optionally narrowed it appears as a query parameter (/api/v1/scans?repositoryId=…).
Free-text search
GET /api/v1/packages accepts search, which matches against the package name. It is a
substring match, not a query language — no operators, no wildcards, no field prefixes.
curl "https://api.ospulse.app/api/v1/packages?search=lodash&take=20" \
-H "Authorization: Bearer $OSPULSE_TOKEN"Combine it with ecosystem to disambiguate names that exist in more than one registry.
For questions that a substring match cannot express, POST /api/v1/ai/query accepts a
natural-language question over your tenant's data. It is a different tool with different
guarantees — useful for exploration, not for a deterministic integration.
Time-window filters
A few endpoints narrow by time rather than by attribute. Values are ISO-8601 UTC and must be URL-encoded.
| Endpoint | Parameter | Effect |
|---|---|---|
GET /api/v1/scans/{scanId}/logs | sinceUtc | Return log entries after an instant — the basis of a tailing loop. |
GET /api/v1/compliance/controls/{controlId}/evidence | periodStart, periodEnd | Bound evidence to a reporting period. |
GET /api/v1/cra/clocks/due | withinHours | Reporting clocks falling due inside a rolling window. |
curl "https://api.ospulse.app/api/v1/scans/$SCAN_ID/logs?sinceUtc=2026-03-14T09%3A21%3A47Z&take=200" \
-H "Authorization: Bearer $OSPULSE_TOKEN"Time-window filters are the one case where re-reading a collection is genuinely safe under
concurrent writes: advance sinceUtc to the timestamp of the last entry you processed and
the next call returns only what is new.
Result-shaping parameters
Not every query parameter is a filter. Some bound the work the endpoint does, and they behave differently — exceeding the bound truncates the result rather than returning an error.
| Endpoint | Parameter | Effect |
|---|---|---|
GET /api/v1/repositories/{repositoryId}/dependency-graph | maxEdges | Caps graph traversal so a deep transitive tree cannot produce an unbounded response. |
GET /api/v1/analytics/export/destinations/{id}/runs | limit | Caps the number of recent runs returned. |
GET /api/v1/cra/reports/{packId} | format | Selects the output format of the downloaded artefact. |
Sorting
The API does not expose a general sort parameter. Each collection endpoint applies an ordering appropriate to its resource — most commonly newest-first for event-like collections such as scans, logs, and alerts, and a stable natural ordering for catalogue-like collections such as packages.
Consequences for a client:
- Do not depend on order beyond what the reference states. An ordering that is not documented is an implementation detail and may change.
- Sort client-side for presentation. Fetch the page, then order it for display. This is correct for a UI and cheap at typical page sizes.
- Do not sort client-side to rank a whole collection. Sorting one page of 50 out of 428 rows ranks that page, not the collection. To find the highest-severity items, filter by severity — do not fetch an arbitrary page and sort it.
- For portfolio-wide ranking, use an endpoint built for it. The dashboard and analytics
endpoints —
GET /api/v1/dashboards/global,GET /api/v1/analytics/trends,GET /api/v1/repositories/{repositoryId}/risk-summary— return computed rankings and aggregates directly, without paging a collection to reconstruct them.
Combining filters with pagination
Filters and pagination compose. Both are query parameters and both apply to the same request:
curl "https://api.ospulse.app/api/v1/packages?ecosystem=Npm&search=react&take=50&skip=0" \
-H "Authorization: Bearer $OSPULSE_TOKEN"The order of operations matters when reading the response:
- Filters are applied first, producing the matching set.
skipandtakeare applied to that filtered set.totalEstimatereflects the filtered set, not the whole collection.
Never change a filter mid-iteration
skip is an offset into the filtered result. Changing any filter between page requests
renumbers every offset, so the pages you have already fetched no longer describe a contiguous
window. Fix the filters before the first request and hold them constant until the loop ends.
Filtering early is also the cheapest optimisation available. Narrowing to one ecosystem before paging is far faster than fetching everything and discarding rows in your client — it reduces database work, response size, and the number of round trips at once, and it keeps you away from the deep offsets that make offset pagination slow.
URL-encoding
Filter values go into a query string and must be percent-encoded. Use your HTTP client's
query builder — URLSearchParams in JavaScript, the params argument in Python's
requests, --data-urlencode with cURL — rather than concatenating strings.
Characters that routinely break hand-built query strings:
| Character | Encoded | Appears in |
|---|---|---|
| space | %20 | Search terms, product names |
@ | %40 | Scoped npm package names |
/ | %2F | Scoped npm names, Go module paths, Maven coordinates |
: | %3A | ISO-8601 timestamps, Maven groupId:artifactId |
+ | %2B | Version metadata. Unencoded, + decodes as a space |
&, = | %26, %3D | Any value containing them terminates the parameter early |
# Wrong — @ and / are interpreted before the server sees them
curl "https://api.ospulse.app/api/v1/packages?search=@angular/core"
# Right — let cURL encode the value
curl -G "https://api.ospulse.app/api/v1/packages" \
--data-urlencode "search=@angular/core" \
--data-urlencode "ecosystem=Npm" \
--data-urlencode "take=25" \
-H "Authorization: Bearer $OSPULSE_TOKEN"const query = new URLSearchParams({
search: '@angular/core',
ecosystem: 'Npm',
take: 25,
})
const res = await fetch(`https://api.ospulse.app/api/v1/packages?${query}`, {
headers: { Authorization: `Bearer ${process.env.OSPULSE_TOKEN}` },
})Identifiers need no encoding — a GUID contains only hexadecimal digits and hyphens — but encoding them anyway is harmless and keeps one code path.
Finding the parameters for an endpoint
- Open the interactive reference and locate the operation.
- Read its Parameters section: name, location (
queryorpath), type, and whether it is required. - For an enum-valued parameter, expand the schema to see the accepted members.
- To script against it, read the same information from
/openapi.json— theparametersarray on each operation is the exact source the reference is rendered from.
