stateweave Docs
Reference

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:

Request headers
GET https://acme.app.stateweave.net/api/v1/me
Authorization: Bearer sw_…
Content-Type: application/json

Create 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).

400 Bad Request
{
  "error": "invalid",
  "problems": ["agent: a label of up to 80 characters, e.g. my-agent@0.3", "timeout: seconds between 60 and 86400"]
}
StatuserrorMeaning
400invalid, invalid_jsonThe request is malformed; problems lists what to fix.
401invalid_api_keyNo key, or a key this workspace doesn't accept.
404not_found, run_errorNo such scenario, test, run or job.
409run_errorThe run has ended (run holds it), or the test needs a re-check.
409busyThe scenario is writing tests or running one, so its data can't change now.
429rate_limitedToo many requests with this key. Wait for Retry-After seconds.
429limit, run_errorA workspace limit: scenarios or tests per day, or runs going at once.

Me

GET/api/v1/me

The workspace and the key.

Response
{"studio": "https://acme.app.stateweave.net", "name": "acme", "key": {"id": "k-3c19a2", "name": "my laptop"}}

Tests

GET/api/v1/tests?project={project}

The tests you can run, every scenario's or one's. Never the answer key or the check.

Response
{
  "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

POST/api/v1/runs
FieldTypeDescription
teststringRequired. The test's id.
agentstringThe label runs are compared by, up to 80 characters. Default agent.
projectstringThe scenario, when the test id is in several.
timeoutnumberSeconds, 60 to 86,400. Default 1,800.
metaobjectShown 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

GET/api/v1/runs/{run}

The run as it stands. While it runs, it includes the apps; once it ends, the verdict.

Finish a run

POST/api/v1/runs/{run}/finish

Body: {"answer": …}, any JSON value, or nothing for an action test. Grades the run, stops its apps and answers with the run as it ended:

Response
{
  "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

POST/api/v1/runs/{run}/cancel

Gives the run up: its apps stop and it is not graded. Answers with the run.

Workers

Wait for a queued run

POST/api/v1/workers/next
FieldTypeDescription
workerstringRequired. Your worker's own id, 4 to 40 letters, digits, - or _. The same on every call.
agentstringRequired. The label its runs get.
metaobjectShown with its runs.
timeoutnumberEach run's time limit. Default: the repeated run's.
waitnumberSeconds 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

POST/api/v1/workers/{worker}/leave

Scenarios

List scenarios

GET/api/v1/projects

{"projects": [...]}, newest first. No credentials.

Get a scenario

GET/api/v1/projects/{project}
Response (shortened)
{
  "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

POST/api/v1/projects
FieldTypeDescription
descriptionstringRequired. The company and what the agent will be tested on.
appsstring[]Required. App names.
options.companystringThe company's name, up to 80 characters.
options.peopleinteger2 to 5,000.
options.weeksinteger1 to 104.
options.contentstringauto, llm or templates.

Answers 201 with the scenario, status generating. Read it until the status changes.

Data

What each app takes

GET/api/v1/projects/{project}/data

{"kinds": {app: {kind: {"title", "fields", "example"}}}, "additions": [...]}. Each field has type, required and help.

Add items

POST/api/v1/projects/{project}/data

Body: {"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

POST/api/v1/projects/{project}/tests

Either your requests, or a count of Claude's ideas:

FieldTypeDescription
requestsarray1 to 10 requests, each a string or {"text", "criteria"?, "kind"?, "apps"?}. Up to 2,000 characters each.
countintegerInstead of requests: 1 to 10 tests from Claude's ideas.
focusstringWith count: what they should be about.
difficultystringnormal or hard.
faultsstring[]Fault preset ids.
noiseobjectInstead: random faults, {"rate", "kinds", "max", "max_hang", "max_outage"}, all optional.
pinnedarrayInstead: up to 20 faults at endpoints, {"app", "endpoint", "kind", "nth"?, "seconds"?, "cut"?}.

Answers 201 with the job.

Get a job

GET/api/v1/projects/{project}/jobs/{job}
Response (shortened)
{
  "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

POST/api/v1/projects/{project}/tests/recheck

Body: {"tests": [...]} to re-check those, or nothing for every stale test. Answers {"rechecking": [test ids]}.

Faults

Presets

GET/api/v1/projects/{project}/faults
Response (shortened)
{
  "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

GET/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.