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.
Authentication
Section titled “Authentication”Every request needs a scoped API key (rz_key_…, issued in Settings →
API keys) as a bearer token:
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.
Upload a dependency snapshot
Section titled “Upload a dependency snapshot”POST /v1/scans/dependency-snapshotsRequired 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.
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{ "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" } ]}| Field | Type | Description |
|---|---|---|
schemaVersion | integer | 1 (legacy, including the npm CLI) or 2 (module-aware Gradle scans). Unknown versions are rejected with 422 invalid_snapshot. |
project | string | Project 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. |
ecosystem | string | One of gradle, python, javascript, typescript. |
manifests | string[] | Every manifest file the snapshot was derived from (a multi-project build has several). |
roots[] | array | Your 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[].module | string | Build-native module id (Gradle project path; ":" is the root project). |
roots[].manifest | string (optional) | The manifest that declares this module’s dependencies. |
roots[].context | string | Which resolution produced these edges — a Gradle configuration name, npm dependencies/devDependencies, a Python extras group. |
roots[].dependencies | string[] | Node ids of the module’s direct third-party dependencies. |
nodes[] | array | Deduplicated third-party packages, sorted by id. |
nodes[].id | string | name@version — the graph-wide key for this package. |
nodes[].name | string | Package coordinates in the ecosystem’s native form. |
nodes[].version | string | Resolved (installed) version, post conflict-resolution. |
nodes[].relationship | string | direct (referenced by at least one root) or transitive. |
nodes[].dependencies | string[] | 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.
{ "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": []}List a project’s scans
Section titled “List a project’s scans”GET /v1/scans?project=prj_6h2m # or omit ?project= for the whole orgRequired 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 a scan
Section titled “Get a scan”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 findings —
suppressed 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.
List a scan’s findings
Section titled “List a scan’s findings”GET /v1/scans/{scanId}/findingsRequired 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.
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: [].
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). Useattributionsfor configuration-specific paths and every introducing declaration.publishedAt— the advisory’s publication date. The plannedfailOn.gracePeriodpolicy will use it.fixedVersion—nullwhen 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.
List a project’s dependencies
Section titled “List a project’s dependencies”Coming soon This inventory endpoint is planned. Use completed scan findings for the available advisory results.
GET /v1/dependencies?project=prj_6h2mRequired 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}Errors
Section titled “Errors”The envelope and common codes (401/403/404/429) are defined in
API conventions. Endpoint-specific codes on this
surface:
| Status | code | Meaning |
|---|---|---|
409 | idempotency_conflict | Same Idempotency-Key, different body. |
413 | snapshot_too_large | Over 10,000 nodes or 10 MB. |
422 | invalid_snapshot | Snapshot failed validation — the message names the offending field, e.g. "dependencies[12].version is required". |
422 | unknown_project | The project isn’t covered by the key’s project restriction (or, on reads, doesn’t exist). |
422 | missing_parameter | A required query parameter was omitted. |