API conventions
Every RingZero API — supply chain scanning today, hosts and future surfaces tomorrow — shares one set of conventions. Endpoint references (like the supply chain API) document only what’s specific to each endpoint; everything on this page applies everywhere.
Base URL and versioning
Section titled “Base URL and versioning”https://api.ringzero.tech/v1The API is versioned in the path. Breaking changes only ever ship as a new
version prefix (/v2); within a version, changes are additive — new fields,
new endpoints, new optional parameters. Write clients that tolerate unknown
fields.
Authentication
Section titled “Authentication”All requests authenticate with a bearer credential:
Authorization: Bearer <credential>Which credential depends on who’s calling — see Authentication & permissions for the full model:
| Credential | Prefix | Used by |
|---|---|---|
| Session token | (managed by the app) | You, signed in — role-gated; the only credential for org, member, invitation, and API-key management |
| API key | rz_key_ | Your integrations — scoped + project-restricted |
| Registration token | rz_reg_ | The sidecar, during host registration only |
| Host credential | (managed by the sidecar) | Heartbeats and challenges for one host |
| Gateway enrollment token | rz_gw_ | A Scan Gateway, once, to enroll |
| Gateway credential | (managed by the gateway) | The gateway’s outbound tunnel |
Requests and responses
Section titled “Requests and responses”JSON bodies both ways (Content-Type: application/json). All timestamps are
ISO-8601 UTC (2026-07-22T09:14:00Z). Ids are prefixed strings (scn_7g8h,
hst_2d8e, prj_6h2m) — treat them as opaque.
Every response carries an X-Request-Id header; quote it when contacting
support about a specific call.
Errors
Section titled “Errors”Errors use one envelope and conventional status codes:
{ "error": { "code": "insufficient_scope", "message": "This endpoint requires 'findings:read'. The key has: sca:upload, scans:read." }}| Status | code | Meaning |
|---|---|---|
401 | invalid_token | Missing, malformed, expired, or revoked credential. |
403 | insufficient_scope | Credential is valid but lacks the endpoint’s required scope — the message names the missing scope. |
404 | not_found | Unknown id — or a resource outside your reach (out-of-reach resources are indistinguishable from nonexistent ones). |
422 | missing_parameter, invalid_cursor, endpoint-specific codes | The request was understood but invalid — the message names the offending field. |
429 | rate_limited | Too many requests — see rate limits. |
Endpoint-specific 422/409/413 codes (like invalid_snapshot or
idempotency_conflict) are documented on their endpoints.
Rate limits
Section titled “Rate limits”120 requests per minute per credential. Every response reports where you stand:
X-RateLimit-Limit: 120X-RateLimit-Remaining: 87X-RateLimit-Reset: 1753178400On 429, honor Retry-After (seconds) with backoff. The Gradle plugin does
this automatically.
Pagination
Section titled “Pagination”List endpoints share one scheme:
limit— page size, default 50, max 200.cursor— opaque token from the previous response’snextCursor. Never construct or modify one.nextCursor: nullmarks the last page.
Cursors are stable across concurrent inserts and expire after 24 hours; an
invalid or expired cursor returns 422 invalid_cursor. Each endpoint
documents its ordering (e.g. findings by severity, scans newest-first).
Endpoint index
Section titled “Endpoint index”| Endpoint | Scope | Reference |
|---|---|---|
POST /v1/scans/dependency-snapshots | sca:upload | Upload a dependency snapshot |
GET /v1/scans?project= | scans:read | List a project’s scans |
GET /v1/scans/{scanId} | scans:read | Get a scan |
GET /v1/scans/{scanId}/findings | findings:read | List a scan’s findings |
GET /v1/dependencies?project= | deps:read | List a project’s dependencies |
GET /v1/hosts | hosts:read | Hosts (below) |
GET /v1/gateways | gateways:read | Gateways (below) |
POST /v1/gateways/enrollments | enrollment token | Gateway-facing — see scan gateway |
POST /v1/hosts/attestations | registration token | Sidecar-facing — see how registration works |
POST /v1/hosts/{hostId}/heartbeats | host credential | Sidecar-facing |
GET /v1/meta/ip-ranges | none (public) | Meta (below) |
POST /v1/mcp/scans | mcp:upload | MCP usage auditing |
POST /v1/mcp/usage-events | mcp:upload | MCP usage auditing |
GET /v1/mcp/servers | mcp:read | MCP security overview |
GET /v1/mcp/registry | mcp:read | MCP Registry |
GET /v1/mcp/control-level | mcp:read | MCP Registry |
PUT /v1/mcp/control-level | session (Owner) | MCP Registry |
GET /v1/me | session | Me & orgs (below) |
GET /v1/members | session | Members & invitations (below) |
PATCH /v1/members/{memberId} · DELETE /v1/members/{memberId} | session (Admin/Owner) | Members & invitations |
GET/POST /v1/invitations · DELETE /v1/invitations/{invitationId} | session (Admin/Owner) | Members & invitations |
POST /v1/invitations/lookups | none (public) | Members & invitations |
POST /v1/invitations/acceptances | session | Members & invitations |
GET/POST /v1/api-keys · DELETE /v1/api-keys/{keyId} | session (Admin/Owner) | API keys (below) |
GET /v1/hostsRequired scope: hosts:read
The org’s registered-host inventory — the API view of Settings → Hosts.
Standard pagination; optional status filter (active, expired,
revoked) and label filter (label=env=production, repeatable).
{ "hosts": [ { "id": "hst_2d8e", "hostname": "web-1.acme.internal", "status": "active", "lastHeartbeatAt": "2026-07-22T09:14:32Z", "labels": { "env": "production", "service": "acme-web" }, "connectivity": "gateway", "gatewayId": "gw_1a2b" } ], "nextCursor": null}connectivity is direct or gateway; gatewayId is set only for gateway-
mode hosts (the Scan Gateway that serves
them). Typical use: fleet dashboards and drift alerts — page through and flag
hosts whose status left active. Registration, revocation, and token
management are role-gated to humans in the app; no API key can perform them.
The two sidecar-facing endpoints (/v1/hosts/attestations, heartbeats) are
called only by the sidecar with its own credentials — they’re listed above for
transparency, and their behavior is documented in
how security scanning works.
Gateways
Section titled “Gateways”GET /v1/gatewaysRequired scope: gateways:read
The org’s Scan Gateway inventory — the API
view of Settings → Gateways. Standard pagination; optional status filter
(pending, active, expired, revoked).
{ "gateways": [ { "id": "gw_1a2b", "name": "acme-prod-vpc", "status": "active", "networkScope": ["10.0.0.0/8"], "version": "1.0.3", "lastSeenAt": "2026-07-22T09:14:32Z" } ], "nextCursor": null}POST /v1/gateways/enrollments is gateway-facing: a gateway exchanges its
one-time rz_gw_ enrollment token for a persistent credential and appears
pending until an admin approves its networkScope. Enrollment, approval, and
revocation are role-gated to humans in the app; no API key can perform them.
GET /v1/meta/ip-rangesUnauthenticated. The source ranges RingZero’s control plane uses for
outbound connections to your infrastructure — the
direct-mode challenge connections
to sidecar port 7301, so you can allowlist tighter than 0.0.0.0/0:
{ "challenge_sources": ["198.51.100.0/28", "2001:db8:4f2::/64"], "updated_at": "2026-06-30T00:00:00Z"}Range changes are announced at least 30 days in advance. This applies to direct mode only — gateway mode has no inbound challenge, so there’s nothing to allowlist.
Me & orgs
Section titled “Me & orgs”Org, member, invitation, and API-key management endpoints are session-only:
they authenticate with your signed-in session and are gated by your
role. No API-key scope grants them — a key gets
403 insufficient_scope. In v1 each user belongs to one org.
RingZero access is request-gated: there is no public signup and no self-serve org creation. Companies request access, we send account-creation details, and organizations are provisioned for them — so the only self-service way into an org is redeeming an invitation.
GET /v1/meThe session→membership bridge — who you are in your org. org and member
are null (together) until you create or join an org:
{ "user": { "id": "5b3e1c9a-…", "email": "dana@acme.com" }, "org": { "id": "org_9f3k", "name": "Acme Corp" }, "member": { "id": "mem_3c9d", "role": "admin", "joinedAt": "2026-03-02T10:00:00Z" }}Roles are lowercase on the wire (owner | admin | member | auditor).
org and member are null together until you accept an invitation. There
is no endpoint for creating an organization — orgs are provisioned by RingZero
when a company’s access request is approved.
Members & invitations
Section titled “Members & invitations”GET /v1/membersThe org’s member list (any role may list; managing is gated). Standard
pagination; ordered by role rank (owner first) then email; optional role=
filter.
{ "members": [ { "id": "mem_3c9d", "userId": "5b3e1c9a-…", "email": "dana@acme.com", "role": "admin", "joinedAt": "2026-03-02T10:00:00Z" } ], "nextCursor": null}PATCH /v1/members/{memberId} (Admin/Owner) sets { "role": "admin" | "member" | "auditor" } — promoting to admin, or changing anyone who is
currently an owner, is Owner-only; owner itself is not assignable
(ownership transfer is a future endpoint). DELETE /v1/members/{memberId}
removes a member — your own id means leaving the org (any role). Demoting or
removing the org’s sole Owner returns 409 last_owner.
Invitations are email-bound, single-use, and expire after 7 days:
POST /v1/invitations(Admin/Owner; inviting asadminis Owner-only) —{ "email": "kim@acme.com", "role": "member" }→201with{ invitation, token, inviteUrl }. Therz_inv_token and invite URL are returned exactly once; re-inviting a pending email revokes and replaces the earlier invite.GET /v1/invitations(Admin/Owner) — standard pagination,status=filter (pending | accepted | revoked | expired), newest first. Responses never contain tokens.DELETE /v1/invitations/{invitationId}(Admin/Owner) — revoke,204.POST /v1/invitations/lookups— unauthenticated preview for the accept page; body{ "token": "rz_inv_…" }→ the org name, invited email, role, status, and expiry. Unknown token →404 not_found. Tokens always ride the POST body, never a URL.POST /v1/invitations/acceptances— session; body{ "token": "rz_inv_…" }joins the inviting org →{ org, member }. Errors:404 not_found,409 invite_expired | invite_revoked | invite_already_accepted | invite_email_mismatch | already_in_org.
API keys
Section titled “API keys”The management plane for scoped API keys — the API view of Settings → API keys. Session-only, Admin/Owner.
GET /v1/api-keysStandard pagination; optional status filter (active | expired | revoked);
newest first. Responses carry the truncated prefix, never the secret;
revoked keys stay listed.
{ "apiKeys": [ { "id": "key_8m3q", "name": "ci-acme-web", "prefix": "rz_key_5f2a…", "scopes": ["sca:upload", "scans:read"], "projects": ["prj_6h2m"], "status": "active", "createdBy": "mem_3c9d", "createdAt": "2026-05-14T08:00:00Z", "expiresAt": "2027-05-14T08:00:00Z", "lastUsedAt": "2026-07-22T09:14:32Z", "revokedAt": null } ], "nextCursor": null}POST /v1/api-keys — { "name", "scopes", "projects": "all" | [prj ids], "expiresAt"? } → 201 with { apiKey, secret }. The full rz_key_ secret
appears exactly once in this response and is never retrievable again. An
unknown scope → 422 invalid_scope; empty name or scopes →
422 missing_parameter. DELETE /v1/api-keys/{keyId} revokes immediately
(204) — in-flight requests with the key fail with 401 invalid_token from
that moment.