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 — 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:
| Route | Limit |
|---|---|
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/scans | 5 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-Afterheader (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 & path | What it does |
|---|---|
GET /api/v1/runs | Lists 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/scans | Triggers a scan and returns immediately with a run name — poll the route above for the result. |
POST /api/v1/scansaccepts 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 actually 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"}| Status | Meaning |
|---|---|
400 | The request body failed validation (e.g. no target, or i_am_authorized wasn't confirmed). |
401 | Missing, malformed, wrong, or revoked API key. |
404 | An unknown run name, or a path this API doesn't define. |
429 | Rate 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-exploitationwas 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
- Authorization & approval — the full gate this API's scan trigger goes through, unchanged.
- Getting started — running Ryvx from the CLI directly, still the primary path for CI/CD.
← Back to Docs