REST API reference
The supply chain API is the surface the Gradle plugin talks to. Use it directly to integrate ecosystems without a first-party plugin, 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 | Snapshot schema version, currently 1. Unknown versions are rejected with 422 invalid_snapshot. |
project | string | Project id (prj_…) or slug. An unknown slug returns 422 unknown_project — projects are never auto-created from uploads. |
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. |
Because the upload is a full graph, the server derives every dependency
path from it — the path you see on
findings is computed server-side, and every route
to a vulnerable package is available, not one arbitrary chain.
Idempotency. Send an optional Idempotency-Key header (any client token,
≤128 chars — the Gradle plugin sends 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.
Limits. A snapshot may contain at most 10,000 nodes and 10 MB of
body; beyond either, 413 snapshot_too_large. Trim with
excludedModules / excludeProjects
rather than splitting one build across snapshots — each snapshot is one scan,
and splitting forks severity counts.
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}List a project’s scans
Section titled “List a project’s scans”GET /v1/scans?project=prj_6h2mRequired scope: scans:read
All scans for a project, newest first — the read that “latest scan for main”
dashboards need. Query: project (required, id or slug), optional status
filter, standard pagination.
{ "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}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.
On failed, failureReason distinguishes “fix my payload”
("unsupported_ecosystem") from “retry later” ("internal_error"). failed
is terminal — re-upload rather than re-polling.
Polling guidance: poll every 5 seconds (the Gradle plugin’s
upload.pollInterval default) with an
overall cap — the plugin uses 5 minutes
(upload.timeout). Most snapshot
scans complete in under a minute.
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).
curl "https://api.ringzero.tech/v1/scans/scn_7g8h/findings?severity=high" \ -H "Authorization: Bearer $RINGZERO_API_KEY"Response — 200 OK
{ "findings": [ { "id": "fnd_9b4c", "scanId": "scn_7g8h", "packageRef": "dep_7f2e", "severity": "high", "cve": "CVE-2025-4410", "publishedAt": "2026-07-18T00:00:00Z", "installedVersion": "1.2.68", "fixedVersion": "1.2.83", "reachable": true, "path": ["build.gradle", "acme-web-core", "fastjson"], "suppressed": false, "summary": "fastjson deserialization of untrusted input can lead to remote code execution." } ], "nextCursor": null}Field notes:
path— the dependency chain from the manifest to the vulnerable package: the why it’s here, and which direct dependency to bump.publishedAt— the advisory’s publication date; drives the plugin’sfailOn.gracePeriodand “new this week” views.fixedVersion—nullwhen no fixed release exists yet.reachable— present only when reachability analysis ran for the ecosystem:truemeans the vulnerable code is reachable from first-party code. Absent means unknown, not unreachable.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”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 id/slug doesn’t exist (or isn’t in the key’s project restriction). |
422 | missing_parameter | A required query parameter was omitted. |