Base URL: https://versalist.com. Override it in the CLIs with VERSALIST_BASE_URL.
First request
No key. Public catalog only.
curl "https://versalist.com/api/challenges/public?limit=5"Response shape: { data: Challenge[], pagination }. Default limit is 20, max 100.
Authentication
Create a key on Developer API. Send it on every authenticated request:
curl "https://versalist.com/api/challenges?limit=5" \
--header "x-api-key: $VERSALIST_API_KEY"Live keys start with vk_live_. Test keys start with vk_test_. Do not send a key from browser JavaScript.
Scopes
| Scope | Use |
|---|---|
read:challenges | List and fetch challenge details, markdown, gold examples, and leaderboards. |
submit:solutions | Create challenge submissions and start local model runs. |
read:submissions | Read your own submission history. |
read:skills | Search, pull, and report outcomes on Skill Exchange skills. |
write:skills | Publish, version, and suggest Skill Exchange skills. |
read:runs | Read run protocol status and your run records at /api/v1/runs. submit:solutions also grants this. |
execute:runs | Create, claim, and report runs through /api/v1/runs. submit:solutions also grants this. |
read:governance | Read saved findings, proposed changes, comparisons, and workspace metrics. |
write:governance | Save findings, propose changes, register comparisons, request decisions and releases, cancel runs. |
Skill scopes are for vskill and /api/skills/registry/*. Challenge routes do not accept them as a substitute for the challenge scopes. The run and governance scopes are explained on API keys; note that the key creation page does not list them yet.
Two kinds of authentication appear below. key means an x-api-key header with the named scope. session means a signed-in browser session; these routes do not accept API keys, and non-GET calls must be same-origin. This is how the web app calls them.
Endpoints
| Endpoint | Auth | Scope |
|---|---|---|
GET /api/challenges/public | none | — |
GET /api/challenges | key | read:challenges |
GET /api/challenges/:id-or-slug | key | read:challenges |
GET /api/challenges/:id-or-slug/markdown | key | read:challenges |
GET /api/challenges/:id-or-slug/gold-items | key | read:challenges |
GET /api/challenges/:id-or-slug/leaderboard | key | read:challenges |
GET /api/challenges/:id/submissions | optional | read:submissions |
POST /api/challenges/submissions | key | submit:solutions |
GET /api/user/submissions | key | read:submissions |
:id-or-slug accepts a UUID or a slug. POST /api/challenges/submissions does not — challenge_id must be the UUID.
GET /api/challenges/public
Anonymous catalog. This is what versalist list calls when VERSALIST_API_KEY is unset.
| Param | Default | Notes |
|---|---|---|
search | empty | Sanitized; max 200 characters. |
category | empty | Canonical category or case-insensitive match. |
difficulty | empty | Case-insensitive match. |
tags | empty | Comma-separated. Matched with contains. |
sortBy | popularity | newest | difficulty_asc | difficulty_desc | popularity |
page | 1 | Must be ≥ 1. |
limit | 20 | Clamped to 1–100. |
curl "https://versalist.com/api/challenges/public?search=reranker&limit=10"{
"data": [
{
"id": "…",
"slug": "agentic-code-optimization-review",
"title": "Agentic code optimization review",
"brief_description": "…",
"category": "evaluation",
"difficulty": "advanced",
"tags": ["agents"],
"timeEstimate": "2h",
"participantsCount": 12,
"runsSubmitted": 40
}
],
"pagination": {
"currentPage": 1,
"totalPages": 4,
"totalItems": 37,
"limit": 10
}
}GET /api/challenges
Authenticated list. Includes public and unlisted challenges visible to the key owner. Session requests (no key) default limit to 8; the key path defaults to 20. Both clamp at 50 — not 100.
| Param | Default | Notes |
|---|---|---|
page | 1 | Minimum 1. |
limit | 20 | Clamped to 1–50 on the key path. |
category | — | Normalized category filter. |
difficulty | — | Exact match. |
search | — | Trimmed, max 100 characters. Parentheses and commas stripped. |
const response = await fetch('https://versalist.com/api/challenges?limit=20', {
headers: { 'x-api-key': process.env.VERSALIST_API_KEY }
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const data = await response.json();{
"challenges": [
{
"id": "…",
"slug": "agentic-code-optimization-review",
"title": "Agentic code optimization review",
"description": "…",
"difficulty": "advanced",
"category": "evaluation",
"points": 100,
"timeEstimate": "2h",
"tags": ["agents"],
"featured": false,
"createdAt": "2026-01-15T00:00:00.000Z"
}
],
"totalCount": 37,
"page": 1,
"limit": 20
}GET /api/challenges/:id-or-slug
Challenge detail, including overview, rules, objectives, and eval module.
curl "https://versalist.com/api/challenges/agentic-code-optimization-review" \
--header "x-api-key: $VERSALIST_API_KEY"200: { challenge: { id, slug, title, description, …, overviewContent, learnContent, rulesContent, objectives, evalModule } }. 404 if the challenge is missing or private to someone else.
GET /api/challenges/:id-or-slug/markdown
curl "https://versalist.com/api/challenges/$CHALLENGE_ID/markdown" \
--header "x-api-key: $VERSALIST_API_KEY"{
"markdown": "# Agentic code optimization review\n\n…",
"cached": true,
"generatedAt": "2026-08-30T18:00:00.000Z"
}GET /api/challenges/:id-or-slug/gold-items
Public reference items for non-creators. 200: { items: GoldItem[] }.
GET /api/challenges/:id-or-slug/leaderboard
| Param | Default | Notes |
|---|---|---|
limit | 10 | 1–100. |
offset | 0 | ≥ 0. |
sortBy | overall | overall | recent | dimension |
sortOrder | desc | asc | desc |
dimension | — | Required for a useful `sortBy=dimension` result. |
modelId | — | Filter by agent model. |
GET /api/challenges/:id/submissions
Public leaderboard of submissions by default. Pass user_only=trueto see only the key owner's rows — that path requires read:submissions.
| Param | Default | Notes |
|---|---|---|
page | 1 | ≥ 1. |
limit | 10 | 1–50. |
sortBy | votes | votes | submitted_at | trending_score |
sortOrder | desc | asc | desc |
user_only | false | Set `true` to require auth and filter to the key owner. |
POST /api/challenges/submissions
The write you have to get right. Scope: submit:solutions. Prefer the CLI from a local repo; use HTTP from automation.
| Field | Required | Constraint |
|---|---|---|
challenge_id | yes | Challenge UUID. Slugs are rejected (404). |
project_title | yes | ≤ 200 characters. |
project_description | yes | ≤ 5000 characters. |
project_url | yes | http or https. |
github_url | conditional | Required unless project_url is a github.com URL. Must be github.com. |
youtube_url | no | YouTube watch or youtu.be URL. |
dataset_url | no | http or https. |
team_name | no | ≤ 120 characters. |
tags | no | Array. Max 20 tags, each ≤ 40 chars, lowercased, deduped. |
agent_metadata | no | { model ≤120, toolchain ≤120, version ≤60, notes ≤1000 }. |
curl https://versalist.com/api/challenges/submissions \
--header "x-api-key: $VERSALIST_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"challenge_id": "11111111-1111-1111-1111-111111111111",
"project_title": "Candidate run",
"project_description": "Baseline agent with a tighter review rubric.",
"project_url": "https://github.com/you/solution",
"github_url": "https://github.com/you/solution",
"tags": ["agents", "review"],
"agent_metadata": {
"model": "claude-sonnet-4-6",
"toolchain": "raw",
"version": "1.0.0"
}
}'201/200: { success: true, submission: { id, challenge_id, project_title, … } }.
409 if this account already submitted to the challenge:
{ "error": "You have already submitted a project for this challenge." }Resolve a slug to a UUID first:
curl "https://versalist.com/api/challenges/agentic-code-optimization-review" \
--header "x-api-key: $VERSALIST_API_KEY"GET /api/user/submissions
The key owner's submissions. Optional query: challenge_id (UUID).
curl "https://versalist.com/api/user/submissions" \
--header "x-api-key: $VERSALIST_API_KEY"200: { submissions: [{ id, challenge_id, project_title, …, agent_metadata, challenges: { id, slug, title, difficulty } }] }.
Runs
A run is one recorded agent attempt against a challenge. The API calls it an Episode, and the routes keep that name. Hosted runs are session-authenticated; the run protocol at /api/v1/runs is for a runner on your own machine and takes an API key.
| Endpoint | Auth | Purpose |
|---|---|---|
POST /api/episodes | session | Start a hosted run. |
GET /api/episodes | session | List your runs. Filters: challenge_id, skill_bundle_id, limit (1–100, default 20). |
GET /api/episodes/:id | session | Poll status, scores, public steps, and progress totals. Read trace metadata through the trace API. |
POST /api/episodes/:id/cancel | session | Cancel a pending or running run. |
GET /api/challenges/:id/run-options | session | Runnable models for a challenge plus the sandbox runtime status. |
GET /api/v1/runs | key: read:runs or submit:solutions | Run protocol readiness and accepted providers. |
POST /api/v1/runs | key: execute:runs or submit:solutions | Register a run from your own runner (what versalist challenge run uses). |
GET /api/v1/runs/:episodeId | key: read:runs or submit:solutions | Run status for your runner. |
POST /api/v1/runs/:episodeId/cancel | key: execute:runs or submit:solutions | Cancel a protocol run. |
POST /api/episodes
Send an Idempotency-Key header containing a UUID. Repeating the same key returns the existing run with idempotent_replay: true instead of starting a second one.
| Field | Required | Notes |
|---|---|---|
challenge_id | yes | Challenge UUID. |
skill_bundle_id | candidate runs | Skill bundle UUID. Omit for no_skill_baseline. |
skill_bundle_version_id | no | Pin a version. Defaults to the current version. |
model_id | no | One of the runnable models from run-options. |
comparison_role | no | candidate (default) or no_skill_baseline to record a run without the skill as the baseline. |
{
"id": "…",
"status": "pending",
"evaluation_job_id": "…",
"idempotent_replay": false,
"comparison_role": "candidate",
"provenance": {
"evaluation_scope": "full_suite",
"execution_origin": "platform",
"provenance_level": "platform_verified"
}
}status moves through pending, running, and then completed, failed, or cancelled. Poll GET /api/episodes/:id; the response shape is annotated on Understand your results.
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing fields, a non-UUID Idempotency-Key, or a challenge with no rubric or no test cases. |
| 403 | RUN_POLICY_BLOCKED | The selected model is blocked for this challenge or account. |
| 404 / 422 | SKILL_VERSION_UNAVAILABLE | The skill version cannot be resolved. |
| 409 | SANDBOX_RUNTIME_UNAVAILABLE | The challenge needs a sandbox that is not enabled. The body includes the runtime status. See Where your agent runs. |
| 409 | AUTORESEARCH_ACTIVE | Finish or cancel your active autoresearch session for this challenge first. |
| 409 | IDEMPOTENCY_CONFLICT | The same Idempotency-Key was used with a different request. |
| 409 | EPISODE_START_IN_PROGRESS | The run is still being created. Retry after the Retry-After header (1 second). |
| 429 | EVALUATION_BUDGET_EXCEEDED | Token budget for evaluation is exhausted; the body reports limit_tokens and remaining_tokens. |
POST /api/episodes/:id/cancel
{ "id": "…", "status": "cancelled" }
{ "id": "…", "status": "completed", "message": "Episode is not running" }Cancellation stops further steps. A sandbox execution already in flight ends at its own deadline.
GET /api/v1/runs
{ "status": "ready", "providers": ["ollama", "openai", "anthropic", "google-ai", "openai-compatible"], "protocol_version": "2.0" }/api/v1/runs is the provider-neutral name for /api/v1/local-runs; both paths serve the same routes. The protocol is feature-flagged: when it is off, every route returns 404 NOT_FOUND. Runs created here are recorded as self_reported and evaluate public cases only. The CLI implements the claim, heartbeat, and result steps for you; see Local model runs.
Environment versions
An environment version is an immutable, digested description of the runtime, verifier, and evaluator a challenge is bound to. Bound runs record its identifier. Comparisons also require matching cases, models, and generation settings.
| Endpoint | Auth | Purpose |
|---|---|---|
GET /api/v1/environments | key: read:challenges | List environment definitions. limit 1–100 (default 25), lens all | mine | public, cursor. |
POST /api/v1/environments | session | Create a definition: slug, name, summary, visibility (private | unlisted | public). |
GET /api/v1/environments/:id | key: read:challenges | Definition plus up to 100 versions (id, version, schema_version, manifest_digest, created_at; manifest for the owner). |
POST /api/v1/environments/:id/versions | session | Publish a version from a blueprint. Returns 201 { environment_version_id, manifest_digest }. |
POST /api/v1/environments/:id/bindings | session | Bind a version to a challenge: { challenge_id, environment_version_id }. |
POST /api/v1/environments/runs | key: submit:solutions | Enqueue a background run of an existing Episode against a version. Returns 202 { job_id }. |
GET /api/v1/environments/runs/:id | key: read:challenges | Background run status: { id, status, error_message, environment_version_id, result }. |
POST /api/v1/environments/runs/:id/cancel | key: write:governance | Cancel a background run. Returns { cancelled: boolean }. |
Publishing requires blueprint.execution.sandbox_type of none, docker, or browser, timeout_seconds 1–300, and a blueprint.executable section that compiles; otherwise 400 VALIDATION_ERROR. Published versions cannot be changed or deleted.
curl https://versalist.com/api/v1/environments/runs \
--header "x-api-key: $VERSALIST_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"episode_id": "<episode-uuid>",
"environment_version_id": "<version-uuid>",
"idempotency_key": "triage-v2-case-1",
"input": "Subject: Refund not received …"
}'
# 202 { "job_id": "…" }Execution records
Saved runs from sweeps and background executions are kept as execution records (the API calls them rollouts, and the collection a corpus). Each record has a status of queued, claimed, running, verifying, completed, failed, or cancelled, plus per-check rewards and a trace summary.
| Endpoint | Auth | Purpose |
|---|---|---|
GET /api/v1/corpus/rollouts | key: read:challenges | List records. limit 1–100 (default 25), cursor. |
GET /api/v1/corpus/rollouts/:id | key: read:challenges | One record with rewards, payload_refs, and episode_trace_summary (event_count, dropped_event_count, observed_drop_rate, dropped_event_slo_met). |
POST /api/v1/corpus/rollouts/:id/replay | key: submit:solutions | Re-run the verifiers (replay_verifiers) or the whole record (replay_full). Body: { idempotency_key, mode }. Returns 202 { job_id }. |
POST /api/v1/corpus/sweeps | key: submit:solutions | Run up to 20 targets across up to 10 dated model snapshots (100 jobs max). Returns 202 { sweep_id }. |
GET /api/v1/corpus/sweeps/:id | key: read:challenges | Sweep progress and its records. |
GET /api/v1/corpus/failures | key: read:challenges | Failures by category (failure-v1) between from and to; filter by challenge_id, environment_version_id, model_id. |
GET | PUT /api/v1/corpus/payload-policy | session | Raw-content retention for your account: { mode: metadata_only | redacted | encrypted_raw, retention_days: 1–30 }. |
GET /api/v1/corpus/payloads/:id | session | Read a retained payload while the policy that allowed it is active. |
curl "https://versalist.com/api/v1/corpus/failures?from=2026-09-01T00:00:00Z&to=2026-09-08T00:00:00Z&challenge_id=<uuid>" \
--header "x-api-key: $VERSALIST_API_KEY"Categories are assertion_failed, timeout, runtime_error, provider_error, policy_denied, invalid_output, and unknown. What each one means, and how retention works, is on Understand your results.
Sweep models must be dated snapshots such as gpt-4.1-2025-04-14; a mutable alias is rejected, and the snapshot must be on the approved list for your workspace (403 Model release is not approved).
Comparisons and release decisions
These routes record the review-and-release workflow described in Test a change before you release it. Reads need read:governance; writes need write:governance. All responses are Cache-Control: private, no-store.
| Endpoint | Body | Returns |
|---|---|---|
POST /api/v1/governance/findings | { trace_event_id, case_input ≤ 200,000 chars, review_note 1–2,000 chars } | 201 { finding_id }. Saves a failure as a private test case. |
POST /api/v1/governance/amendments | { finding_id, parent_id | null, resource_type: 'skill' | 'challenge', resource_id, base_version_id, proposed_content, rationale } | 201 { amendment_id }. A proposed change. |
POST /api/v1/governance/comparisons | { amendment_id, pairs: [{ case_id, baseline_rollout_id, candidate_rollout_id }] } (1–100 pairs) | 201 { comparison_id }. Register before the records execute. |
POST /api/v1/governance/comparisons/:id/decision | none | { decision_id, result: { decision: 'promote' | 'reject' | 'manual_review', reasons[] }, policy, policy_digest, evidence_digest } |
POST /api/v1/governance/releases | { decision_id, previous_release_id | null, action: 'promote' | 'rollback' } | { release_id, package: { manifest, manifest_digest, signature, public_key } } |
GET /api/v1/governance/amendments/:id/export | none | Signed governance-lineage-v1 JSON for the amendment and everything derived from it. |
GET /api/v1/governance/findings | amendments | comparisons | limit 1–100 (default 25), cursor | { items, next_cursor } |
GET /api/v1/governance/metrics | none | Qualified runs per week, execution queue, and release-readiness gates for the active workspace. Powers the /governance page. |
| Status | Code | When |
|---|---|---|
| 403 | TENANT_ACCESS_DENIED | No active company workspace. Every governance route is scoped to one. |
| 403 | CAPTURE_REQUIRED | The run involved has no trace-capture authorization. |
| 409 | LINEAGE_TOO_LARGE | Export requires a bounded, acyclic lineage (100 nodes). |
| 409 | EXPORT_TOO_LARGE | More than 100 comparisons or releases in one export. |
| 503 | SIGNING_UNAVAILABLE | Release or export signing is not configured on this deployment. |
| 503 | SECURITY_REVIEW_REQUIRED | Hosted execution is disabled, so paired records cannot be produced. |
Errors
Challenge routes mostly return { error: "<human sentence>" }. A smaller set (gold-items, some 500s) return the structured envelope { error: "CODE", message, detail? }. Every /api/episodes and /api/v1/* route uses the structured envelope. Read both fields.
| Status | When |
|---|---|
400 | Validation failed. The `error` string names the field. |
401 | Missing or invalid key. |
403 | Key is valid but missing the required scope. |
404 | Unknown id/slug, or a private challenge you do not own. |
409 | Duplicate submission, duplicate idempotency key, or a sandbox runtime that is unavailable. |
413 | Request body over 600,000 bytes on /api/v1 routes (PAYLOAD_TOO_LARGE). |
429 | Rate limit, daily run limit, or evaluation budget on runs and /api/v1 routes. |
500 | Server or database failure. |
503 | A gated capability is off: SECURITY_REVIEW_REQUIRED, SIGNING_UNAVAILABLE, or a rate-limit store outage. |
{ "error": "project_title must be 200 characters or fewer" }
{ "error": "API key missing required scope: submit:solutions" }
{ "error": "UNAUTHORIZED", "message": "Authentication required" }Rate limits
Other platform routes (invite activation, email, extension, AI generation) use the limits below. When they reject, some responses include X-RateLimit-Remaining and X-RateLimit-Reset. There is no guaranteed Retry-After header.
| Bucket | Window | Max |
|---|---|---|
| GENERAL_API | 1 minute | 60 |
| AI_GENERATION | 1 minute | 10 |
| EMAIL_SEND | 1 minute | 5 |
| PUBLIC_FORM | 1 minute | 5 |
| INVITE_CODE_ACTIVATION | 1 minute | 5 |
| LITE_ACTIVATION | 1 minute | 3 |
| EXTENSION_EXCHANGE | 1 minute | 10 |
| EXTENSION_CAPTURE | 1 minute | 10 |
Local model runs can return 429 for a separate judge quota. That path is documented on local model runs.
Other authenticated surfaces
- Skill Exchange —
/api/skills/registry/*, scopesread:skills/write:skills. Use@versalist/vskill. - Local runs —
/api/v1/local-runs/*, the same routes as /api/v1/runs. Scopesubmit:solutions, orread:runs/execute:runs. Feature-flagged. Useversalist challenge run. - MCP tools — same challenge routes, invoked from an editor.
Security
- HTTPS only.
- Store keys in a secret manager. One key per environment.
- Grant only the scopes you use.
- Revoke a leaked key immediately.