Skip to content

REST API reference

The supply chain API is the surface the Gradle plugin and the npm CLI talk to. Use it directly to integrate ecosystems without a first-party client, or to pull findings into your own tooling.

Platform-wide behavior — base URL, auth, error envelope, rate limits, pagination — is documented once in API conventions; this page covers the endpoints.

Every request needs a scoped API key (rz_key_…, issued in Settings → API keys) as a bearer token:

Terminal window
curl https://api.ringzero.tech/v1/scans/scn_7g8h \
-H "Authorization: Bearer $RINGZERO_API_KEY"

Keys carry an explicit scope list and a project restriction, both fixed at creation — each endpoint below states its required scope. The full model is on Authentication & permissions.

POST /v1/scans/dependency-snapshots

Required scope: sca:upload

Uploads the resolved dependency graph for one project — optionally with suppressions — and queues a scan. The snapshot is deliberately ecosystem-generic: the same nodes-and-edges shape works for Gradle, Python, and JavaScript/TypeScript, so the server can process every ecosystem identically. It should reflect the graph as your build resolves it — after version conflict resolution — not the raw manifest declarations.

Terminal window
curl -X POST https://api.ringzero.tech/v1/scans/dependency-snapshots \
-H "Authorization: Bearer $RINGZERO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9f2c81d4e7a3b6f0" \
-d @snapshot.json
snapshot.json
{
"schemaVersion": 1,
"project": "prj_6h2m",
"ecosystem": "gradle",
"manifests": ["acme-web-core/build.gradle.kts", "build.gradle.kts"],
"roots": [
{
"module": ":",
"manifest": "build.gradle.kts",
"context": "runtimeClasspath",
"dependencies": ["org.springframework.boot:spring-boot-starter-web@3.4.2"]
},
{
"module": ":acme-web-core",
"manifest": "acme-web-core/build.gradle.kts",
"context": "runtimeClasspath",
"dependencies": ["com.alibaba:fastjson@1.2.68"]
}
],
"nodes": [
{
"id": "com.alibaba:fastjson@1.2.68",
"name": "com.alibaba:fastjson",
"version": "1.2.68",
"relationship": "direct",
"dependencies": []
},
{
"id": "org.springframework.boot:spring-boot-starter-web@3.4.2",
"name": "org.springframework.boot:spring-boot-starter-web",
"version": "3.4.2",
"relationship": "direct",
"dependencies": ["org.springframework:spring-web@6.2.9"]
},
{
"id": "org.springframework:spring-web@6.2.9",
"name": "org.springframework:spring-web",
"version": "6.2.9",
"relationship": "transitive",
"dependencies": []
}
],
"suppressions": [
{
"cve": "CVE-2025-4410",
"package": "com.alibaba:fastjson",
"reason": "Not exploitable: we never deserialize untrusted input",
"expires": "2026-10-01"
}
]
}
FieldTypeDescription
schemaVersioninteger1 (legacy, including the npm CLI) or 2 (module-aware Gradle scans). Unknown versions are rejected with 422 invalid_snapshot.
projectstringProject id (prj_…) or slug, as configured in your build (ringzero { project = … } / RINGZERO_PROJECT). A new slug creates the project on first upload. 422 unknown_project means the key’s project restriction doesn’t cover this project — a restricted key can never create projects or upload outside its list.
ecosystemstringOne of gradle, python, javascript, typescript.
manifestsstring[]Every manifest file the snapshot was derived from (a multi-project build has several).
roots[]arrayYour first-party modules — one entry per scanned module and resolution context. First-party code appears only here, never in nodes, so it is never advisory-matched.
roots[].modulestringBuild-native module id (Gradle project path; ":" is the root project).
roots[].manifeststring (optional)The manifest that declares this module’s dependencies.
roots[].contextstringWhich resolution produced these edges — a Gradle configuration name, npm dependencies/devDependencies, a Python extras group.
roots[].dependenciesstring[]Node ids of the module’s direct third-party dependencies.
nodes[]arrayDeduplicated third-party packages, sorted by id.
nodes[].idstringname@version — the graph-wide key for this package.
nodes[].namestringPackage coordinates in the ecosystem’s native form.
nodes[].versionstringResolved (installed) version, post conflict-resolution.
nodes[].relationshipstringdirect (referenced by at least one root) or transitive.
nodes[].dependenciesstring[]Node ids of this package’s direct dependencies. Every ref must resolve within nodes — a dangling ref is 422 invalid_snapshot.
suppressions[]array (optional)Same shape as the suppressions file: cve + reason required, package glob and expires optional. Covered by sca:upload — suppressions always travel with the snapshot they apply to.

The server derives finding paths from the uploaded graph. Legacy v1 scans retain one shortest display path. V2 preserves each module/configuration’s resolved graph and derives every distinct introducing dependency, with one shortest path per introduction. It does not enumerate every possible path through a dependency cycle or diamond.

Snapshot v2: modules and resolution graphs

Section titled “Snapshot v2: modules and resolution graphs”

The JSON Schema describes the v2 wire format. V2 retains the fields above and requires modules and a resolution on every root. nodes[].id must equal name@version. A module can appear in the graph without having an independently scanned root.

snapshot-v2.json
{
"schemaVersion": 2,
"project": "maps-service",
"ecosystem": "gradle",
"manifests": ["app/build.gradle.kts"],
"modules": [
{"path": ":core", "dependencies": [], "teamName": "maps", "attributes": {"domain": "geospatial"}},
{"path": ":app", "dependencies": [":core"], "teamName": "mobile"}
],
"roots": [{
"module": ":app", "context": "runtimeClasspath", "dependencies": [],
"resolution": {"root": "v0", "vertices": [
{"id": "v0", "module": ":app", "dependencies": ["v1"]},
{"id": "v1", "module": ":core", "dependencies": ["v2"]},
{"id": "v2", "node": "com.alibaba:fastjson@1.2.68", "dependencies": []}
]}
}],
"nodes": [{"id": "com.alibaba:fastjson@1.2.68", "name": "com.alibaba:fastjson", "version": "1.2.68", "relationship": "transitive", "dependencies": []}],
"suppressions": []
}

Vertex ids and edges are local to one resolution. Each vertex identifies exactly one module (module) or external package (node). Separate vertices can point to the same package id when different selected variants expose different dependencies. The backend keeps those variants separate during attribution, so edges cannot leak between configurations or module consumers.

The resolution.root must identify the root’s module. Every edge, package, and module reference must exist, every vertex must be reachable from its root, and every package must occur in a resolution. Duplicate node/vertex ids and duplicate module/context roots are rejected. The API validates these cross-references in addition to the JSON Schema’s structural checks.

For v2, the resolution graphs are authoritative. The flat dependencies, node relationship, and optional producer nodes[].attributions are compatibility/report views; the worker does not use them to assign ownership. A module-to-package edge identifies an introducing dependency. This is the selected dependency after resolution, not an original requested version or source line. Constraints do not count as introductions.

Modules are ordered dependencies before consumers; cycles form alphabetical groups, with alphabetical ties between ready groups. teamName and attributes are snapshot-local labels, not team membership or authorization grants. Missing teams remain unassigned. Metadata and finding attributions are replaced together when processing completes.

Single-document v2 limits. This endpoint accepts 1,000 modules, 1,000 contexts, 100,000 total vertices, 500,000 total edges, and 256 shortest dependency hops. The sum across roots of (vertices + edges) × distinct packages must not exceed 50 million, bounding attribution work even if every package has a finding. Size/work limits return 413 snapshot_too_large; malformed graphs return 422 invalid_snapshot. Team names are at most 200 characters; each module can supply 50 string attributes (keys 100 characters, values 2,000). Derived paths also have a per-scan output budget of 8 million characters and 1 million segments. Exceeding it fails the scan with attribution_limit_exceeded instead of returning incomplete ownership; use the multipart upload flow for larger graphs.

The Gradle plugin emits v2. Existing v1 producers remain accepted; old scans keep their paths and severity totals but have empty module attribution. Run a new scan with v2 to capture ownership; historical ownership is not inferred.

Idempotency. Send an optional Idempotency-Key header (any client token, ≤128 chars — the Gradle plugin and the npm CLI send the SHA-256 hex of the exact body bytes). Replaying the same key with an identical body within 24 hours returns the original scan with 200 instead of creating a duplicate (201); the same key with a different body returns 409 idempotency_conflict. This is what keeps retried uploads and racing CI jobs from forking your scan history.

Single-document body limits. This endpoint accepts at most 10,000 nodes and 10 MB of body; beyond either, 413 snapshot_too_large. For large builds, use multipart uploads to keep the whole repository in one scan. Multipart has per-request and resource budgets instead of these repository-wide counts. The Gradle plugin selects multipart automatically when the backend supports it.

Response — 201 Created (or 200 for an idempotent replay)

{
"id": "scn_7g8h",
"target": "acme-web",
"status": "queued",
"findings": null,
"suppressed": null,
"startedAt": "2026-07-22T09:14:00Z",
"finishedAt": null,
"failureReason": null,
"modules": []
}
GET /v1/scans?project=prj_6h2m # or omit ?project= for the whole org

Required scope: scans:read

All scans, newest first — the read that “latest scan for main” dashboards need. Query: project (optional, id or slug — omit it to list every scan in the organization), optional status filter, standard pagination. Keys with a project restriction see only their projects’ scans either way.

{
"scans": [
{ "id": "scn_7g8h", "target": "acme-web", "status": "completed", "…": "" }
],
"nextCursor": null
}
GET /v1/scans/{scanId}

Required scope: scans:read

{
"id": "scn_7g8h",
"target": "acme-web",
"status": "completed",
"findings": { "critical": 0, "high": 1, "medium": 3, "low": 9 },
"suppressed": 2,
"startedAt": "2026-07-22T09:14:00Z",
"finishedAt": "2026-07-22T09:15:42Z",
"failureReason": null,
"modules": [
{ "path": ":core", "dependencies": [], "teamName": "maps", "attributes": { "service": "json-client" } },
{ "path": ":app", "dependencies": [":core"], "attributes": {} }
]
}

status is one of queued, running, completed, failed. Severity counts are null until the scan completes, and exclude suppressed findingssuppressed carries their count separately, so the gate’s view and the totals stay reconcilable.

The example above is an inline v2 scan: module metadata is included in modules. A multipart scan instead returns attributionMode: "paged", moduleCount, and modules: []. Fetch its metadata from GET /v1/scans/{scanId}/modules; an empty compatibility array does not mean it has no modules. Older v1 and not-yet-processed inline scans also return an empty list. Multiple affected modules do not multiply severity counts.

See incremental reads for module search, team lists, finding attributions, and dependency paths.

On failed, failureReason distinguishes an unsupported ecosystem ("unsupported_ecosystem"), excessive attribution output ("attribution_limit_exceeded"; reduce scanned configurations), and a processing error ("internal_error"; retry later). failed is terminal — re-upload rather than re-polling. For multipart scans, consult the worker diagnostics and budgets.

Polling guidance: custom clients should poll every 5 seconds with an overall timeout, such as 5 minutes. These match the planned ringzeroCheck defaults for upload.pollInterval and upload.timeout. The current ringzeroScan task returns after upload acceptance and does not poll for findings.

GET /v1/scans/{scanId}/findings

Required scope: findings:read

Query parameters: severity (at-or-above filter), suppressed (default false — suppressed findings are hidden, matching the gate’s view; true = only suppressed; all = both), standard pagination (ordered severity-desc, then id).

For module-aware scans, add module=:core and/or team=maps. Both filters must match the same module. attribution=affected (default) selects consuming modules; attribution=introduced selects the module that declares an introducing dependency, even when the vulnerable package is transitive. Filters apply before pagination and combine with severity and suppression filters. Unknown labels return an empty list. Scan/org/project authorization is unchanged by these labels.

The app’s Show dependencies control starts in the introducing view; API clients must send attribution=introduced explicitly to match it. See reviewing findings by module and team for an example of how the two views differ.

Terminal window
curl "https://api.ringzero.tech/v1/scans/scn_7g8h/findings?team=maps&attribution=introduced" \
-H "Authorization: Bearer $RINGZERO_API_KEY"

For paged scans, request details=summary for the findings list, then fetch /findings/{findingId}/attributions and each attribution’s /path as needed. Follow nextCursor until it is null; reset cursors when filters change. Use severityMode=exact for a single severity instead of the default at_least. The response’s counts contains unsuppressed severity totals and a suppressed total for the team/module scope, before severity and suppression-visibility filters. See the paged API contract.

For inline v2 scans, each finding adds attributions, ordered by module graph then configuration:

{
"module": ":app",
"context": "runtimeClasspath",
"relationship": "transitive",
"path": [":app", ":core", "com.acme:json-client@1", "com.alibaba:fastjson@1.2.68"],
"origins": [{
"module": ":core",
"dependency": "com.acme:json-client@1",
"path": [":app", ":core", "com.acme:json-client@1", "com.alibaba:fastjson@1.2.68"]
}]
}

relationship describes the consuming module’s shortest path. A direct path can coexist with origins from other module declarations. origins keeps one path per introducing module and selected dependency, including independent declarations in different modules. Join module paths to the scan’s modules for team labels and custom attributes. Findings remain unique per advisory, package name, and installed version. V1 findings return attributions: [].

Terminal window
curl "https://api.ringzero.tech/v1/scans/scn_7g8h/findings?severity=high" \
-H "Authorization: Bearer $RINGZERO_API_KEY"

Response — 200 OK (legacy v1 scan)

{
"findings": [
{
"id": "fnd_9b4c",
"scanId": "scn_7g8h",
"severity": "high",
"cve": "CVE-2025-4410",
"publishedAt": "2026-07-18T00:00:00Z",
"package": "com.alibaba:fastjson",
"installedVersion": "1.2.68",
"fixedVersion": "1.2.83",
"path": ["build.gradle", "acme-web-core", "fastjson"],
"suppressed": false,
"suppressionReason": null,
"attributions": [],
"summary": "fastjson deserialization of untrusted input can lead to remote code execution."
}
],
"nextCursor": null
}

Field notes:

  • package — the vulnerable package in the ecosystem’s native coordinates.
  • path — a representative chain (legacy scans start with a manifest; v2 scans use module paths and full package ids). Use attributions for configuration-specific paths and every introducing declaration.
  • publishedAt — the advisory’s publication date. The planned failOn.gracePeriod policy will use it.
  • fixedVersionnull when no fixed release exists yet.
  • reachable — reserved for future runtime reachability analysis. The current processor does not emit it for either snapshot version. Absent means unknown, not unreachable; dependency paths alone do not establish runtime reachability.
  • packageRef (dep_…) will join the shape when the dependency inventory ships.
  • suppressed / suppressionReason — suppressed findings (when requested via the query param) carry the reason from the suppression that matched.

Coming soon This inventory endpoint is planned. Use completed scan findings for the available advisory results.

GET /v1/dependencies?project=prj_6h2m

Required scope: deps:read

The current dependency inventory (from the latest completed scan), ordered by name, standard pagination. project is required — omitting it returns 422 missing_parameter.

{
"dependencies": [
{
"id": "dep_7f2e",
"project": "prj_6h2m",
"ecosystem": "gradle",
"name": "com.alibaba:fastjson",
"version": "1.2.68",
"direct": false,
"path": ["build.gradle", "acme-web-core", "fastjson"],
"manifest": "build.gradle"
}
],
"nextCursor": null
}

The envelope and common codes (401/403/404/429) are defined in API conventions. Endpoint-specific codes on this surface:

StatuscodeMeaning
409idempotency_conflictSame Idempotency-Key, different body.
413snapshot_too_largeOver 10,000 nodes or 10 MB.
422invalid_snapshotSnapshot failed validation — the message names the offending field, e.g. "dependencies[12].version is required".
422unknown_projectThe project isn’t covered by the key’s project restriction (or, on reads, doesn’t exist).
422missing_parameterA required query parameter was omitted.