Skip to content

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.

https://api.ringzero.tech/v1

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

All requests authenticate with a bearer credential:

Authorization: Bearer <credential>

Which credential depends on who’s calling — see Authentication & permissions for the full model:

CredentialPrefixUsed by
Session token(managed by the app)You, signed in — role-gated; the only credential for org, member, invitation, and API-key management
API keyrz_key_Your integrations — scoped + project-restricted
Registration tokenrz_reg_The sidecar, during host registration only
Host credential(managed by the sidecar)Heartbeats and challenges for one host
Gateway enrollment tokenrz_gw_A Scan Gateway, once, to enroll
Gateway credential(managed by the gateway)The gateway’s outbound tunnel

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 use one envelope and conventional status codes:

{
"error": {
"code": "insufficient_scope",
"message": "This endpoint requires 'findings:read'. The key has: sca:upload, scans:read."
}
}
StatuscodeMeaning
401invalid_tokenMissing, malformed, expired, or revoked credential.
403insufficient_scopeCredential is valid but lacks the endpoint’s required scope — the message names the missing scope.
404not_foundUnknown id — or a resource outside your reach (out-of-reach resources are indistinguishable from nonexistent ones).
422missing_parameter, invalid_cursor, endpoint-specific codesThe request was understood but invalid — the message names the offending field.
429rate_limitedToo many requests — see rate limits.

Endpoint-specific 422/409/413 codes (like invalid_snapshot or idempotency_conflict) are documented on their endpoints.

120 requests per minute per credential. Every response reports where you stand:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1753178400

On 429, honor Retry-After (seconds) with backoff. The Gradle plugin does this automatically.

List endpoints share one scheme:

  • limit — page size, default 50, max 200.
  • cursor — opaque token from the previous response’s nextCursor. Never construct or modify one.
  • nextCursor: null marks 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).

EndpointScopeReference
POST /v1/scans/dependency-snapshotssca:uploadUpload a dependency snapshot
GET /v1/scans?project=scans:readList a project’s scans
GET /v1/scans/{scanId}scans:readGet a scan
GET /v1/scans/{scanId}/findingsfindings:readList a scan’s findings
GET /v1/dependencies?project=deps:readList a project’s dependencies
GET /v1/hostshosts:readHosts (below)
GET /v1/gatewaysgateways:readGateways (below)
POST /v1/gateways/enrollmentsenrollment tokenGateway-facing — see scan gateway
POST /v1/hosts/attestationsregistration tokenSidecar-facing — see how registration works
POST /v1/hosts/{hostId}/heartbeatshost credentialSidecar-facing
GET /v1/meta/ip-rangesnone (public)Meta (below)
POST /v1/mcp/scansmcp:uploadMCP usage auditing
POST /v1/mcp/usage-eventsmcp:uploadMCP usage auditing
GET /v1/mcp/serversmcp:readMCP security overview
GET /v1/mcp/registrymcp:readMCP Registry
GET /v1/mcp/control-levelmcp:readMCP Registry
PUT /v1/mcp/control-levelsession (Owner)MCP Registry
GET /v1/mesessionMe & orgs (below)
GET /v1/memberssessionMembers & 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/lookupsnone (public)Members & invitations
POST /v1/invitations/acceptancessessionMembers & invitations
GET/POST /v1/api-keys · DELETE /v1/api-keys/{keyId}session (Admin/Owner)API keys (below)
GET /v1/hosts

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

GET /v1/gateways

Required 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-ranges

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

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/me

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

GET /v1/members

The 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 as admin is Owner-only) — { "email": "kim@acme.com", "role": "member" }201 with { invitation, token, inviteUrl }. The rz_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/lookupsunauthenticated 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.

The management plane for scoped API keys — the API view of Settings → API keys. Session-only, Admin/Owner.

GET /v1/api-keys

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