Quickstart · REST API
The REST API
Everything the web app does is a JSON call under https://vvdexops.com/api/v1. The full schema is at /api/openapi.json (OpenAPI; load it into any OpenAPI tool).
Authenticate
Create a key in the web app under Settings → API keys and send it on every request:
$ export VVDEX_API_KEY=vvx_…
$ curl -s https://vvdexops.com/api/v1/me -H "Authorization: Bearer $VVDEX_API_KEY"A key acts for its workspace. It can read and run everything in it, but it cannot create API keys or delete the account; those need a signed-in browser.
Refer to a model
Wherever a model is expected, send {"connection_id": "…", "model": "…"}. GET /models lists the ones you saved.
1 · Connect and check a model
$ curl -s https://vvdexops.com/api/v1/connections \
-H "Authorization: Bearer $VVDEX_API_KEY" -H "Content-Type: application/json" \
-d '{"provider": "openai", "label": "OpenAI", "api_key": "sk-…"}'
# → {"id": "con_…", "provider": "openai", "key_hint": "…abcd", "has_key": true, …}
$ curl -s https://vvdexops.com/api/v1/connections/con_…/check \
-H "Authorization: Bearer $VVDEX_API_KEY" -H "Content-Type: application/json" \
-d '{"model": "gpt-4.1"}'
# → {"verdict": "ok" | "warn" | "fail", "findings": [{"level", "check", "detail", "kind"}], "model"}The key is encrypted on arrival and never returned; later reads show only its last four characters. GET /providers lists every provider id; use custom with a base_url for any OpenAI-compatible address, or http for an app.
2 · Compare in the playground
$ curl -s https://vvdexops.com/api/v1/playground \
-H "Authorization: Bearer $VVDEX_API_KEY" -H "Content-Type: application/json" \
-d '{"messages": [{"role": "user", "content": "Name three prime numbers."}],
"models": [{"connection_id": "con_…", "model": "claude-sonnet-4-5"},
{"connection_id": "con_…", "model": "gpt-4.1"}]}'
# → {"id": "pg_…", "results": [{"model", "ok", "text", "latency_ms", "input_tokens",
# "output_tokens", "cost_usd", "finish_reason", "error"}, …]}cost_usd is null when the price is unknown. A failed call has ok: false and an error.kind such as auth, timeout or rate_limit.
3 · Start an evaluation
$ curl -s https://vvdexops.com/api/v1/evals \
-H "Authorization: Bearer $VVDEX_API_KEY" -H "Content-Type: application/json" \
-d '{"name": "nightly incident triage",
"source": {"forge": "vvdex.ops.gateway-incident-1", "attempts": 3},
"models": [{"connection_id": "con_…", "model": "gpt-4.1"}]}'
# → {"id": "…", "status": "queued"}GET /forge/exams lists the exams and their limits. For your own data, use "source": {"dataset_id": "…"} and add "graders": [{"type", "params", "weight"}]; GET /graders lists them. Optional: judge (a model reference), system_prompt, params (temperature, max_tokens) and repeats (1 to 5).
4 · Read the result
$ curl -s https://vvdexops.com/api/v1/evals/RUN_ID -H "Authorization: Bearer $VVDEX_API_KEY"
# status, progress {done, total}, and per model: n_scored, n_passed, pass_rate,
# wilson_low, wilson_high, mean_score, errors {kind: count}, cost_usd, latency_ms {p50, p95}, judged
$ curl -s "https://vvdexops.com/api/v1/evals/RUN_ID/results?status=failed&limit=20" \
-H "Authorization: Bearer $VVDEX_API_KEY"
$ curl -s "https://vvdexops.com/api/v1/evals/compare?a=RUN_A&b=RUN_B" -H "Authorization: Bearer $VVDEX_API_KEY"
$ curl -s https://vvdexops.com/api/v1/evals/RUN_ID/receipt -H "Authorization: Bearer $VVDEX_API_KEY" > receipt.jsonErrors are counted per kind and left out of n_scored, so they never lower a pass rate.
Endpoints
| Area | Endpoints |
|---|---|
| Account | GET /me · GET /usage · GET /plans · GET /audit |
| Models | GET /providers · GET|POST /connections · DELETE /connections/{id} · POST /connections/{id}/check · GET|POST /models · DELETE /models/{id} |
| Playground | POST /playground · GET /playground · POST /playground/{id}/share |
| Datasets | GET|POST /datasets · GET|DELETE /datasets/{id} · POST /datasets/{id}/rows · POST /datasets/{id}/upload · PATCH|DELETE /datasets/{id}/rows/{row_id} · GET /graders |
| Evaluations | GET|POST /evals · GET /evals/{id} · GET /evals/{id}/results · POST /evals/{id}/cancel · GET /evals/compare · POST /evals/{id}/share · GET /evals/{id}/receipt · GET /evals/{id}/export.csv · POST /evals/{id}/publish |
| Judge calibration | POST /evals/{id}/results/{result_id}/label · GET /evals/{id}/agreement |
| Forge exams | GET /forge/exams · GET /leaderboards · GET /leaderboards/{exam} |
| What we tested | GET /research/campaigns · GET /research/campaigns/{id} · GET /research/models · GET /research/exams/{exam} |
| Public | POST /receipts/verify · GET /receipts/public-key · GET /status |
Errors and limits
A refused request returns {"detail": "…"} in plain words, with the HTTP status:
400the input is not usable (the detail says why) ·422a field has the wrong type401no key, or a key that is wrong or revoked ·403the action needs a signed-in browser, or your role is read-only404not found, or not in your workspace429a rate limit or a plan limit was reached; for a plan limit the detail names it and says when it resets
Rate limits per workspace: 300 playground runs an hour, 60 new connections an hour, 120 connection checks an hour. Workspace limits are on the limits page.