Skip to content

Quickstart

This page takes you from an API key to a finished run over plain HTTP, with curl. To do the same from Claude Code, Cursor or Codex, connect them to the MCP server instead: Connect your coding agent has the setup and the same first run as prompts.

Every request goes to https://api.saram.sh with Authorization: Bearer <your key>. The HTTP API reference has every operation in full.

Sign up or sign in at dashboard.saram.sh. Signing up creates your organisation. Mint a key under Settings > API keys. The dashboard shows it once: Saram keeps only a hash of it, so a lost key is revoked and replaced, never shown again.

Put it in your shell, never in a file you commit:

Terminal window
export API_KEY=<your key>

Check it with who_am_i, the one route every key may call:

Terminal window
curl -s https://api.saram.sh/api/v1/me -H "Authorization: Bearer $API_KEY"

The answer names the key’s caller, its organisation (tenant, with its plan) and what it may do (scopes). To start runs, the key needs runs:write; to read them, runs:read. Storing a model key needs credentials:write, and billing needs keys:admin.

A run sends personas through your product. Each one is named by its registry address, id@version:

Terminal window
curl -s https://api.saram.sh/api/v1/personas -H "Authorization: Bearer $API_KEY"
{
"personas": [
{
"address": "dana_first_timer@1",
"id": "dana_first_timer",
"version": 1,
"name": "Dana Whitfield",
"pack": "general",
"tags": ["general", "novice", "desktop", "first_time"]
}
]
}

The list above is cut to its first entry. get_persona answers one persona in full: its goals, its tolerance and the question it answers at the end.

The personas and the judge make model calls. On the Free and Pro plans they are made on your own provider: store a key from Anthropic, OpenAI, OpenRouter or another supported provider, and the run spec names it. Store one under Settings > Credentials on the dashboard, or over the API:

Terminal window
jq -n --arg v "$ANTHROPIC_API_KEY" '{name: "anthropic", kind: "anthropic", value: $v}' |
curl -s -X POST https://api.saram.sh/api/v1/credentials \
-H "Authorization: Bearer $API_KEY" -H 'content-type: application/json' -d @-

The answer’s secret_ref is what the spec names. The key itself is never returned. Providers and your own keys covers every provider, the ChatGPT login, and which model each role uses.

A run spec says where to go (target), who goes (personas), what they try (missions) and how far each may go (budget). Save this as spec.json, with your own URL, the secret_ref from step 3, and in place of /account a part of the URL your product shows once someone is signed in:

{
"target": { "entry_url": "https://staging.example.com" },
"personas": ["dana_first_timer@1"],
"missions": [
{
"id": "sign_up",
"goal": "create an account",
"success_criteria": [
{ "kind": "url_matches", "value": "/account" },
{ "kind": "judge_assertion", "value": "the persona reached a signed-in page" }
]
}
],
"budget": { "steps_per_session": 30 },
"providers": {
"entries": {
"main": { "model": "anthropic:claude-opus-5", "credential_ref": "<your secret_ref>" }
}
}
}

Only the url_matches criterion is checked by machine: when the persona says it is finished, the session ends success_criteria_met if the last URL it was on contains /account, and claimed_finished_unverified if not. The judge_assertion is text the judge reads beside the goal. Without a url_matches criterion, every finished session ends unverified (Success criteria: what is checked).

The URL must be one Saram’s browsers reach over the internet: a staging deployment, a preview, or a tunnel to your machine (Testing localhost and previews). The run spec explains every field.

validate_run runs every check that starting a run runs, and starts nothing. It costs nothing, so call it before every run:

Terminal window
curl -s -X POST https://api.saram.sh/api/v1/runs/validate \
-H "Authorization: Bearer $API_KEY" -H 'content-type: application/json' -d @spec.json

A spec that passes answers 200 with the spec as the run would store it, the sessions it plans (one per persona per mission), any warnings, and a cost_ceiling:

{
"cost_ceiling": {
"usd": 0.6,
"sessions": 1,
"steps_per_session": 30,
"basis": "up to $0.60: 1 session x 30 persona steps at $0.02 a step on the pilot plan, which bills steps; judging and reproduction are included. ...",
"model_calls": "Model calls, never in this figure: ..."
},
"sessions": [
{
"session_id": "<run_id>.sess_01_dana_first_timer",
"persona_id": "dana_first_timer@1",
"mission_id": "sign_up"
}
],
"warnings": []
}

This answer is cut to its main fields, and it is from an organisation on an older plan that bills persona steps; on Free and Pro the basis counts Usage instead. cost_ceiling.usd is the most the run can bill on your plan, not a forecast: every step is counted at its most. basis says how it was reached, in your plan’s terms, and model_calls says who bills the model calls. On Free and Pro, usage_ceiling also says whether your balance covers the run (Usage and billing).

A spec that fails answers 422 with a detail naming each field and what is wrong with it, for example a credential_ref that is not one of yours:

run spec rejected: providers entry 'main' names awssm://... which is not this tenant's credential: upload the key with POST /api/v1/credentials and name the ref it returns

Fix the spec and validate again until it answers 200.

Terminal window
curl -s -X POST https://api.saram.sh/api/v1/runs \
-H "Authorization: Bearer $API_KEY" -H 'content-type: application/json' -d @spec.json

create_run answers 202 with the run_id at once, and the run starts in the background:

{ "run_id": "run_2026-10-23_ab12cd", "workflow_id": "..." }

A 422 is a spec that fails a check, with no run created. A 402 is a run your plan cannot pay for now (Usage and billing).

get_run answers the run’s status, which moves pending, running, observing, then stops on completed, failed or cancelled:

Terminal window
RUN=run_2026-10-23_ab12cd
while :; do
status=$(curl -s https://api.saram.sh/api/v1/runs/$RUN \
-H "Authorization: Bearer $API_KEY" | jq -r .status)
echo "$status"
case $status in completed|failed|cancelled) break ;; esac
sleep 15
done

Only completed means there are results to read. failed carries a status_reason. While the run is open, billed_steps and each session’s heartbeat_step climb. To stop a run early, POST /api/v1/runs/$RUN/cancel (signal_run); a cancelled run is not judged.

A run gives two kinds of output, and they are not the same thing (Issues vs persona feedback):

Terminal window
H="Authorization: Bearer $API_KEY"
curl -s https://api.saram.sh/api/v1/runs/$RUN/issues -H "$H" # defects
curl -s https://api.saram.sh/api/v1/runs/$RUN/verdicts -H "$H" # persona feedback
curl -s https://api.saram.sh/api/v1/runs/$RUN/opportunities -H "$H" # persona feedback
curl -s https://api.saram.sh/api/v1/runs/$RUN/tester-errors -H "$H" # the persona's own slips
  • Issues (get_issues) are product defects. Beside them, reading[] says for each one whether may_file_ticket_alone is true: only then did a replay reproduce it, and only then file it as a confirmed bug.
  • Verdicts (get_verdicts) are each persona’s answer at the end of its session, quoted from its own diary. Opportunities (get_opportunities) are what the personas’ behaviour says the product could do better or already does well. Both are opinion: attribute them to the persona.
  • Tester errors (get_tester_errors) are slips the persona made, such as a mistyped password. They are not defects and say what to fix in your test setup.

Each session is recorded unless the spec turns it off. list_sessions gives each session’s video_url, a path on the API that takes the same key:

Terminal window
video=$(curl -s https://api.saram.sh/api/v1/runs/$RUN/sessions -H "$H" | jq -r '.sessions[0].video_url')
curl -s -o session.webm "https://api.saram.sh$video" -H "$H"

The video is a WebM file. The route honours Range and HEAD, so a player can seek in it. Evidence is kept for your plan’s retention period (Usage and billing); the run says when in evidence_expires_at.

Block Checked
GET /me, GET /personas 2026-10-23 against api.saram.sh, platform-api 0.9.0; the answers’ fields as shown, with the account’s own values left out
Storing a model key Not run for this page; the body is checked against the reference’s CredentialWrite schema
Validate, 200 2026-10-23, the spec above without its providers block and its url_matches criterion, on an organisation on a plan that bills steps; the answer is cut to the fields shown
The url_matches criterion Not run for this page; added 2026-10-24 from the RunSpecMission schema in platform-api 0.11.0, which also gives the two end_reasons
Validate, 422 2026-10-23, a providers entry naming a ref that is not the organisation’s; the ref is elided here
Starting a run Not run for this page; the answer’s fields are the reference’s StartedRun
Polling, the read routes and the video 2026-10-23, on a finished run: the status, the four read routes, and HEAD on a video_url (200, video/webm, accept-ranges: bytes)