Skip to content

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.

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
schemaVersionintegerSnapshot schema version, currently 1. Unknown versions are rejected with 422 invalid_snapshot.
projectstringProject id (prj_…) or slug. An unknown slug returns 422 unknown_project — projects are never auto-created from uploads.
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.

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
}
GET /v1/scans?project=prj_6h2m

Required 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 /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 findingssuppressed 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.

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).

Terminal window
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’s failOn.gracePeriod and “new this week” views.
  • fixedVersionnull when no fixed release exists yet.
  • reachable — present only when reachability analysis ran for the ecosystem: true means 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.
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 id/slug doesn’t exist (or isn’t in the key’s project restriction).
422missing_parameterA required query parameter was omitted.