POST/v1/auth/requester/registerpublicCreate a developer account. Returns a bootstrap api_key, shown once.
For agents
Everything an agent needs to order human testing and read the verdict. Humans can do the same things from the dashboard — the API is the product, the UI is a client of it.
export API=https://touchstone-qa.com
export KEY=tst_…Download the OpenAPI spec to generate routes, request/response objects, and API clients. Task instructions and tester reports are also published as versioned JSON Schemas.
# Generate routes and types from the live OpenAPI spec
npx @openapitools/openapi-generator-cli generate \
-i $API/openapi.json -g typescript-fetch -o ./src/touchstone
# Payload JSON Schemas (task instructions / tester reports)
curl -o task-v1.json $API/v1/schemas/task/v1
curl -o report-v1.json $API/v1/schemas/report/v1Developers (you, or your agent) authenticate with an API key: X-API-Key: tst_… Dashboard sessions use Authorization: Bearer <developer JWT> and can call the same endpoints. Testers use their own Bearer JWT from /v1/auth/tester/login.
curl -X POST $API/v1/auth/requester/register \
-H 'Content-Type: application/json' \
-d '{"name": "Acme Agents", "email": "ops@acme.dev", "password": "supersecret"}'
# 201 → {"requester": {...}, "api_key": "tst_…"}
# The raw key is shown exactly once. Store it; only its hash is kept server-side.instructions holds the structured payload, validated against the task/v1 schema: scenario_steps[], survey_questions[], focus_areas[] — at least one required. Upload the build first, then reference it as artifact://<key>, or pass any external URL.
# 1. Ask for an artifact upload slot
curl -X POST $API/v1/uploads/presign \
-H "X-API-Key: $KEY" \
-H 'Content-Type: application/json' \
-d '{"kind": "artifact"}'
# → {"upload_url": "https://…", "key": "artifacts/ab12…", "expires_in": 900}
# 2. PUT the binary straight to the presigned URL
curl -X PUT --upload-file ./app-debug.apk "https://…"
# 3. Reference it on the task as "artifact_url": "artifact://artifacts/ab12…"curl -X POST $API/v1/tasks \
-H "X-API-Key: $KEY" \
-H 'Content-Type: application/json' \
-d '{
"artifact_type": "apk_url",
"artifact_url": "artifact://artifacts/ab12…",
"niche": "mobile_game",
"test_type": "gameplay",
"instructions": {
"scenario_steps": [
"Install and launch the game",
"Complete the tutorial",
"Play one match"
],
"survey_questions": ["Was the tutorial clear?"],
"focus_areas": ["onboarding", "controls"]
},
"targeting": {
"platforms": ["android"],
"locales": ["en-US"],
"device_classes": ["phone"]
},
"credentials": "tester@acme.dev / hunter2",
"ends_at": "2026-08-27T21:00:00Z",
"budget_cents": 1500,
"max_testers": 2
}'
# 201 → the task object. Money is always integer cents of your account currency.
# "ends_at" (required) is the hard deadline; "starts_at" defaults to the
# creation moment when omitted.
# "credentials" (optional, ≤2000 chars) is shown only to the assigned tester.
# "max_testers" (optional, 1–5, default 1) sets how many testers can accept;
# the budget is paid per approved report.
# Budget cap: 5000 cents ($50) / 500000 kopecks (₽5 000) per tester →
# HTTP 422 {"code": "budget_too_high"}.When a tester submits, their report appears in the task detail envelope's reports list. Reports are validated against report/v1:
# Poll until reports appear in the envelope
curl -H "X-API-Key: $KEY" $API/v1/tasks/$TASK_ID
# → {"task": {...}, "reports": [{...}]}
# Approve (pays that tester) or reject with a note — per report
curl -X POST -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
-d '{"note": "Great catch on the tutorial soft-lock."}' \
$API/v1/tasks/$TASK_ID/reports/$REPORT_ID/approvecreated → in_progress (first accept) → submitted (first report) → paid once every submitted report is reviewed and at least one is approved; rejected when all reports are rejected. After shipping fixes, the requester closes the loop: paid/rejected → done with optional fix_notes. Each accept fills one of max_testers slots. Invalid transitions return HTTP 409.
Domain errors return the appropriate HTTP status with a JSON body of {"detail", "code"}. detail is a human-readable English message; code is a stable snake_case identifier — branch on code, never on the message text.
409 Conflict
{"detail": "An active task already exists for this artifact and test type",
"code": "duplicate_task"}Common codes: duplicate_task, insufficient_funds, budget_too_high, invalid_transition, task_already_accepted, no_free_slots, report_already_submitted, report_already_reviewed, payout_hold, amount_exceeds_balance, stripe_connect_required, email_already_registered, invalid_credentials. Validation failures return HTTP 422 with FastAPI's standard field-error list.
Agents with MCP support (Claude Code, Cursor, OpenHands) can call Touchstone as native tools instead of raw HTTP. Point your client at the hosted endpoint, or run the server locally with your own API key:
# Claude Code — one line:
claude mcp add touchstone https://touchstone-qa.com/mcp
# Any MCP client (mcp.json) — stdio runs against your own key:
{
"mcpServers": {
"touchstone": {
"command": "python",
"args": ["-m", "mcp_server"],
"env": {
"TOUCHSTONE_API_KEY": "tst_…",
"TOUCHSTONE_API_URL": "https://touchstone-qa.com/api"
}
}
}
}The same contract as native tools: a JSON catalog with each tool's inputSchema (and outputSchema when known). Use it to generate typed wrappers around the MCP server.
curl -o touchstone-mcp-tools.json https://touchstone-qa.com/mcp/schemaorder_testCreate a test task (POST /v1/tasks); 409s explain balance vs. duplicate.upload_artifactPresign + PUT a local build; returns the artifact:// reference (stdio only).list_tasksList your tasks as {tasks, count}, optional status filter.get_taskFull task detail: task, reports, tester assignments.get_reportThe {task, reports} envelope — bugs, UX findings, metrics.get_artifact_urlShort-lived download URL for the build, filename preserved.cancel_taskCancel an open task; committed budget returns to balance.approve_reportApprove a report, releasing that tester's payout.reject_reportReject a report (note required); payment withheld.mark_task_doneClose a settled task after shipping fixes; optional fix_notes.get_balanceBudget balance in integer cents plus recent top-ups.topup_budgetAdd funds (Stripe Checkout for USD, Alfa for RUB).quote_transferPreview a RUB ↔ USD conversion of your own wallets.convert_balanceConvert amount_cents between your RUB and USD wallets.get_profileRequester profile: name, email, account currency.switch_currencySwitch active currency (RUB/USD) — sub-accounts, nothing converts.get_geoGeo detection: country code and assigned currency.list_api_keysActive API keys (ids, prefixes, labels — never raw keys).create_api_keyIssue a new key; raw tst_… returned once.revoke_api_keyRevoke a key by id.list_notificationsNotification feed as {notifications, count}.get_unread_countHow many unread notifications you have.mark_notification_readMark one notification read.mark_all_notifications_readMark everything read.delete_notificationDelete one notification.clear_notificationsDelete all notifications.get_schemaThe versioned JSON Schema (task/report, v1) for payload validation.Full requester-API parity: every endpoint a developer can call has a matching tool. List-shaped tools return single-JSON envelopes ({tasks, count}); 204 actions return {"status": "ok"}.