Skip to main content

Docs / Public API

Public API

A build server can't hold a browser session. This is what it holds instead.

Every route under /api/* in the local dashboard is that dashboard's own internal API, authenticated by a Supabase session cookie or the bearer JWT it hands your browser after sign-in. That's fine for a logged-in person, useless for a CI/CD job. /api/v1/* is a second, deliberately smaller surface for that case: three routes, authenticated by a long-lived API key instead of a session, meant for a pipeline that wants Ryvx's findings without shelling out to the CLI (which remains the primary, fully-featured way to run Ryvx from CI; see the .github/workflows/ryvx.yml example workflow in the repo). Reach for this API specifically when a script or service, not a person at a terminal, needs to trigger a scan or pull results over HTTP.

Issuing a key

From an already-signed-in dashboard session, POST a label for the key:

curl -X POST http://127.0.0.1:8765/api/settings/api-keys \
  -H "Content-Type: application/json" \
  -d '{"label": "prod CI pipeline"}'

The response's key field (a string starting ryvx_live_...) is shown exactly once, in that response, and nowhere else. Only its hash is ever persisted; there is no "reveal an existing key" endpoint the way there is for the local data-encryption key. Copy it into your CI provider's secret store immediately: a key that's lost is meant to be revoked and replaced, not recovered. GET /api/settings/api-keys lists issued keys by a display fingerprint (e.g. ryvx_live_...a1b2) and revocation status, never the key itself; POST /api/settings/api-keys/<id>/revoke revokes one. All three are session-gated, the same as every other /api/* settings route: issuing or revoking a key is itself an action that requires a human signed in to the dashboard, not something an API key can do to itself.

Authenticating requests

Every /api/v1/* request carries the key as a bearer token:

Authorization: Bearer ryvx_live_...

A missing, malformed, wrong, or revoked key all return the same 401: nothing in the response distinguishes which case you hit. This key is checked ONLY against /api/v1/*; it is a separate credential from the session cookie/JWT that guards the rest of /api/*, and cannot be used to reach any route outside this page's table below.

Rate limits

Limits are per API key, not per source IP: several CI runners sharing one egress IP share nothing here; each key has its own independent budget. Reads and scan-triggering are budgeted separately, because they cost wildly different amounts:

RouteLimit
GET /api/v1/runs, GET /api/v1/runs/<run_name>60 requests/minute per key: a cheap local-disk read; a pipeline polling a running scan every few seconds stays well under this.
POST /api/v1/scans5 requests/10 minutes per key: every call spends real LLM tokens on a full scan against your own API key, so this budget is deliberately strict.

Exceeding either limit returns 429 with a Retry-After header (seconds until the limit's own window rolls over) and the same JSON error shape as every other error below. Back off and retry after that many seconds rather than retrying immediately.

HTTP/1.1 429 Too Many Requests
Retry-After: 37
Content-Type: application/json

{"error": "rate limit exceeded -- slow down and retry after the given interval"}

Session-gated /api/* routes (everything a signed-in dashboard user hits directly) are unaffected: these limits apply only to /api/v1/*.

Routes

/api/v1/ is versioned in the path deliberately: this exact set of routes and response shapes is frozen once shipped. A breaking change becomes /api/v2/, not an edit made in place here.

Method & pathWhat it does
GET /api/v1/runsLists runs: name, status (running/done), finding count, severity buckets, target.
GET /api/v1/runs/<run_name>One run's status and, once done, its full findings array (the same shape the dashboard itself renders from).
POST /api/v1/scansTriggers a scan and returns immediately with a run name: poll the route above for the result.

POST /api/v1/scans accepts the same body fields as the New Scan form's own submit (target, i_am_authorized, scan_mode, environment, run_name, instruction, and so on) and responds 202 with {"run_name": "..."} once the scan process has launched:

curl -X POST http://127.0.0.1:8765/api/v1/scans \
  -H "Authorization: Bearer ryvx_live_..." \
  -H "Content-Type: application/json" \
  -d '{"target": "https://staging.example.com", "i_am_authorized": true,
       "environment": "staging", "scan_mode": "standard"}'

Errors

Every response on this surface is JSON, success or failure, including an unmatched route, which still returns a body rather than an empty 404. Every error takes the same shape:

{"error": "a human-readable message"}
StatusMeaning
400The request body failed validation (e.g. no target, or i_am_authorized wasn't confirmed).
401Missing, malformed, wrong, or revoked API key.
404An unknown run name, or a path this API doesn't define.
429Rate limit exceeded for this key: see Retry-After and the Rate limits section above.

What this API deliberately cannot do

Read Authorization & Approval first if you haven't: every exploit attempt against a production-tagged target requires a live human to say yes, and a non-interactive run auto-denies rather than hanging, unless --auto-approve-exploitation was passed explicitly. An API key is a credential, not a human, so POST /api/v1/scans will never accept anything equivalent to that flag, no matter what the request body contains. There is no auto_approve field, no override, no escalation path on this route. A scan launched through this API against a production-tagged target (the default, same as the CLI) behaves exactly like an unattended CI run does today: recon runs, but every exploit attempt an exploitative subagent tries is auto-denied and logged, not silently skipped.

If your pipeline's scan genuinely doesn't need exploitation to be gated (the same shape as the example CI workflow's PR-triggered source scans, which only ever touch a checked-out copy in the runner), set "environment": "dev" or "staging" in the request body. That's the real, existing opt-out available to any caller, interactive or not; it isn't new or weaker for this API specifically. What this API will not do is let a request body talk a production-tagged scan out of the gate that's supposed to apply to it.

Beyond that one boundary, this is a small, intentionally thin surface: three routes, nothing administrative (no settings, no key management, no encryption controls) is reachable with an API key. That's a scope decision, not a temporary gap: a wider surface is a wider set of promises to keep stable at v1.

Next


← Back to Docs
STAY IN THE LOOP

Release notes and product updates, by email.

We'll send a confirmation email; you're not on the list until you click the link in it. See our privacy policy.