REST API
Everything sw and the Python SDK do goes through this API, so any language can do it
too. Requests and responses are JSON.
Base URL and authentication
The API lives under your workspace's address. Send an API key as a bearer token on every request:
GET https://acme.app.stateweave.net/api/v1/me
Authorization: Bearer sw_…
Content-Type: application/jsonCreate and revoke keys in your workspace under Connect an agent. A key can do everything in its workspace; keep it out of code and logs.
Errors
A refused request answers a 4xx status and a JSON body with error, plus message or
problems (every problem found, so you can fix them all at once).
{
"error": "invalid",
"problems": ["agent: a label of up to 80 characters, e.g. my-agent@0.3", "timeout: seconds between 60 and 86400"]
}| Status | error | Meaning |
|---|---|---|
| 400 | invalid, invalid_json | The request is malformed;
problems lists what to fix. |
| 401 | invalid_api_key | No key, or a key this workspace doesn't accept. |
| 404 | not_found, run_error | No such scenario, test, run or job. |
| 409 | run_error | The run has ended (run holds it), or the test needs
a re-check. |
| 409 | busy | The scenario is writing tests or running one, so its data can't change now. |
| 429 | rate_limited | Too many requests with this key. Wait for
Retry-After seconds. |
| 429 | limit, run_error | A workspace limit: scenarios or tests per day, or runs going at once. |
Me
/api/v1/meThe workspace and the key.
{"studio": "https://acme.app.stateweave.net", "name": "acme", "key": {"id": "k-3c19a2", "name": "my laptop"}}Tests
/api/v1/tests?project={project}The tests you can run, every scenario's or one's. Never the answer key or the check.
{
"tests": [
{
"id": "t-bde537",
"project": "p-041ba8",
"project_title": "Northstar Goods",
"title": "Combined spend of a duplicate shopper",
"prompt": "Ethan Smith looks like he's been buying from us under two different email addresses. …",
"kind": "answer",
"apps": ["shopify"],
"answer_format": "{\"total_paid_usd\": 123.45, \"orders\": [\"NG-1234\", \"NG-1235\"]}",
"persona": {"name": "Priya Castellano", "handle": "priya.castellano", "title": "Customer Operations Coordinator"},
"faults": 0
}
]
}Runs
Start a run
/api/v1/runs| Field | Type | Description |
|---|---|---|
test | string | Required. The test's id. |
agent | string | The label runs are compared by, up to 80 characters. Default
agent. |
project | string | The scenario, when the test id is in several. |
timeout | number | Seconds, 60 to 86,400. Default 1,800. |
meta | object | Shown with the run. Up to 4,000 characters as JSON. |
Answers 201 with the run once its apps are up: the request, the persona and each app's
base_url, endpoints, credentials and mcp. See
The run file.
Get a run
/api/v1/runs/{run}The run as it stands. While it runs, it includes the apps; once it ends, the verdict.
Finish a run
/api/v1/runs/{run}/finishBody: {"answer": …}, any JSON value, or nothing for an action test. Grades the run, stops its apps and
answers with the run as it ended:
{
"id": "r-aee3438d",
"project": "p-041ba8",
"test": "t-bde537",
"test_version": 1,
"test_title": "Combined spend of a duplicate shopper",
"kind": "answer",
"agent": "my-agent@0.1",
"meta": {"model": "my-model"},
"status": "failed",
"created_at": 1793669077.43,
"expires_at": 1793670877.43,
"timeout": 1800.0,
"finished_at": 1793669077.79,
"passed": false,
"reasons": ["total 1.00 != expected 349.10"],
"calls": 9,
"state": {"shopify": {"added": 0, "changed": 0, "removed": 0}},
"url": "https://acme.app.stateweave.net/#/runs/r-aee3438d"
}Cancel a run
/api/v1/runs/{run}/cancelGives the run up: its apps stop and it is not graded. Answers with the run.
Workers
Wait for a queued run
/api/v1/workers/next| Field | Type | Description |
|---|---|---|
worker | string | Required. Your worker's own id, 4 to 40 letters, digits,
- or _. The same on every call. |
agent | string | Required. The label its runs get. |
meta | object | Shown with its runs. |
timeout | number | Each run's time limit. Default: the repeated run's. |
wait | number | Seconds to wait for a run, 0 to 30. Default 25. |
Answers 201 with a started run, as POST /api/v1/runs does, or 204 when none
came in time. Call it again at once: a worker counts as connected for a minute after its last call.
Stop a worker
/api/v1/workers/{worker}/leaveScenarios
List scenarios
/api/v1/projects{"projects": [...]}, newest first. No credentials.
Get a scenario
/api/v1/projects/{project}{
"id": "p-041ba8",
"title": "Northstar Goods",
"description": "Northstar Goods is a 35-person gift retailer …",
"apps": ["slack", "shopify", "gmail"],
"status": "ready",
"error": null,
"steps": [{"key": "draft", "title": "Design the company", "status": "done", "detail": "Northstar Goods: 35 people in 7 departments, …"}],
"envs": {
"slack": {"counts": {"people": 35, "channels": 10, "messages": 1279}, "summary": "35 people, 10 channels, …", "state": "stopped"}
}
}Create a scenario
/api/v1/projects| Field | Type | Description |
|---|---|---|
description | string | Required. The company and what the agent will be tested on. |
apps | string[] | Required. App names. |
options.company | string | The company's name, up to 80 characters. |
options.people | integer | 2 to 5,000. |
options.weeks | integer | 1 to 104. |
options.content | string | auto, llm or
templates. |
Answers 201 with the scenario, status generating. Read it until the status changes.
Data
What each app takes
/api/v1/projects/{project}/data{"kinds": {app: {kind: {"title", "fields", "example"}}}, "additions": [...]}. Each field has
type, required and help.
Add items
/api/v1/projects/{project}/dataBody: {"app", "kind", "items": [...]}. Answers 201 with
{"addition", "counts", "stale"}. A bad item answers 400 with every problem, and nothing is
added.
Writing tests
Write tests
/api/v1/projects/{project}/testsEither your requests, or a count of Claude's ideas:
| Field | Type | Description |
|---|---|---|
requests | array | 1 to 10 requests, each a string or
{"text", "criteria"?, "kind"?, "apps"?}. Up to 2,000 characters each. |
count | integer | Instead of requests: 1 to 10 tests from Claude's ideas. |
focus | string | With count: what they should be about. |
difficulty | string | normal or hard. |
faults | string[] | Fault preset ids. |
noise | object | Instead: random faults, {"rate", "kinds", "max",
"max_hang", "max_outage"}, all optional. |
pinned | array | Instead: up to 20 faults at endpoints, {"app",
"endpoint", "kind", "nth"?, "seconds"?, "cut"?}. |
Answers 201 with the job.
Get a job
/api/v1/projects/{project}/jobs/{job}{
"id": "j-0ff32e",
"project": "p-041ba8",
"status": "done",
"items": [
{"status": "ready", "test_id": "t-5f4aae", "title": "Hand off Tomás and Jenna's non-Harbor tickets", "request": "Tomás and Jenna need to spend …"},
{"status": "rejected", "title": "…", "request": "…", "detail": "why it can't be a test", "suggestion": "a rewording that would work"}
],
"error": null
}Re-check tests
/api/v1/projects/{project}/tests/recheckBody: {"tests": [...]} to re-check those, or nothing for every stale test. Answers
{"rechecking": [test ids]}.
Faults
Presets
/api/v1/projects/{project}/faults{
"presets": [
{
"id": "slack.dropped_page",
"app": "slack",
"kind": "drop",
"observed": true,
"title": "A history page breaks off after 4 KB",
"evidence": "A read of a ~388 KB conversations.history broke off mid-response. …"
}
]
}Where faults can be pinned
/api/v1/projects/{project}/endpoints{"apps": {app: {"title", "kinds", "endpoints"}}, "kinds", "noise": {"defaults", "limits"}}. Each
endpoint has an id (what pinned takes), a label such as
PUT /api/v2/tickets/{ticket_id}, class (read or write) and the
kinds that can happen there.