For agents

API documentation

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_…

Machine-readable schema

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/v1

Authentication

Developers (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.

Endpoints

POST/v1/auth/requester/registerpublic

Create a developer account. Returns a bootstrap api_key, shown once.

POST/v1/auth/requester/loginpublic

Email/password → session JWT for the dashboard.

GET/v1/medeveloper

Your profile: name, email, account currency.

PUT/v1/me/currencydeveloper

Switch active currency. Body: {"currency": "RUB"|"USD"}. Balances are separate per currency — nothing converts.

GET/v1/geopublic

Client geo info: country code and charge currency (RUB/USD).

GET/v1/keysdeveloper

List active API keys (prefix, label, timestamps — never raw keys).

POST/v1/keysdeveloper

Generate a key. Raw key returned once.

DELETE/v1/keys/{id}developer

Revoke a key (soft).

POST/v1/tasksdeveloper

Order a test. Body per task/v1; optional credentials field. Budget ≤ $50 / ₽5 000 per tester; max_testers ≤ 5.

GET/v1/tasks?status=&q=developer

List your tasks, optionally filtered by status and free-text q.

GET/v1/tasks/{id}developer

Task detail as {task, reports[], assignments[]} envelope.

GET/v1/tasks/{id}/reportdeveloper

All non-draft reports for a task.

GET/v1/tasks/{id}/artifact-urldeveloper

Presigned GET for artifact:// uploads, else passthrough URL.

POST/v1/tasks/{id}/reports/{report_id}/approvedeveloper

Approve one tester's report and pay that tester. Optional note.

POST/v1/tasks/{id}/reports/{report_id}/rejectdeveloper

Reject one tester's report. Note required.

POST/v1/tasks/{id}/approve | /rejectdeveloper

Single-report review shortcut (409 when the task has several reports).

POST/v1/tasks/{id}/canceldeveloper

Cancel before resolution; the committed budget returns to your balance.

POST/v1/tasks/{id}/donedeveloper

Close a settled task (paid/rejected) after shipping fixes. Optional fix_notes.

GET/v1/budgetdeveloper

Balance in account currency plus top-up history. Tasks debit the balance on creation.

POST/v1/budget/topupsdeveloper

Create a top-up. Returns the payment form URL to redirect to (null in stub mode — instantly paid).

POST/v1/budget/topups/syncdeveloper

Ask the payment rail whether pending top-ups have been captured; returns the updated budget.

GET/v1/transfers/quoteeither

Preview a RUB ↔ USD conversion of your own wallets. Integer cents; CBR mid-market minus the platform spread.

POST/v1/transferseither

Execute a RUB ↔ USD conversion. Pass expected_credit_cents from the quote; 409 rate_changed if the live credit is worse.

GET/v1/notificationseither

Your notification feed (task status changes, review verdicts), newest first, max 50.

GET/v1/notifications/unread-counteither

How many unread notifications you have: {count}.

POST/v1/notifications/read-all · /{id}/readeither

Mark all — or one — notification as read. 204.

DELETE/v1/notifications · /{id}either

Delete all — or one — notification. 204.

GET/v1/schemas/{name}/{version}public

The versioned JSON Schema (task|report, v1) payloads are validated against.

GET/v1/public/tasks/{id}public

Anonymous task preview (no credentials/artifact URL) — what bot short links open.

GET/s/{code}public

Short redirect from bot broadcasts → the public task page. 307; unknown code → 404.

POST/v1/uploads/presigneither

kind=artifact (developer, optional filename keeps the download name) or recording/screenshot with task_id (tester).

POST/v1/auth/tester/registerpublic

Create a tester account.

POST/v1/auth/tester/loginpublic

Email/password → tester JWT.

GET/v1/tester/metester

Profile, devices, locales, rating, approved-report count, stripe_connected.

PUT/v1/tester/me/devicestester

Replace device list. Body: {"devices": [...]}.

PUT/v1/tester/me/phonetester

Set the payout phone. Body: {"phone": "+79161234567"}.

DELETE/v1/tester/me/phonetester

Remove the payout phone.

PUT/v1/tester/me/currencytester

Switch the default earnings/withdrawal sub-account. Body: {"currency": "RUB"|"USD"}. Balances stay separate — nothing converts.

GET/v1/tester/tasks/availabletester

All open tasks with free tester slots, excluding ones I already accepted.

GET/v1/tester/tasks/minetester

Tasks assigned to me; optional status and q filters.

GET/v1/tester/tasks/mine/counttester

Badge counter: how many of my tasks match the status filter.

GET/v1/tester/tasks/{id}tester

Instructions, credentials, artifact — if assigned or available to me.

POST/v1/tester/tasks/{id}/accepttester

Claim an open task while it has free tester slots.

POST/v1/tester/tasks/{id}/releasetester

Hand back an accepted task before submitting anything.

PUT/v1/tester/tasks/{id}/drafttester

Upsert my private draft report (bugs/ux_findings/metrics).

GET/v1/tester/tasks/{id}/drafttester

Fetch my saved draft for a task.

POST/v1/tester/tasks/{id}/reporttester

Submit a report/v1 body. recording_key required.

GET/v1/tester/tasks/{id}/artifact-urltester

Download URL for the build under test.

GET/v1/transfers/quoteeither

Preview a RUB ↔ USD conversion of your own wallets. Integer cents; CBR mid-market minus the platform spread.

POST/v1/transferseither

Execute a RUB ↔ USD conversion. Pass expected_credit_cents from the quote; 409 rate_changed if the live credit is worse.

GET/v1/tester/earnings?currency=tester

Per-currency ledger: {total_cents, withdrawn_cents, available_cents, payout_hold_reports, withdrawal_fee_cents, entries[]}.

POST/v1/tester/withdrawalstester

Request a payout: {"amount_cents", "currency", "phone"?}. RUB is SBP (phone + FIO + bank_code); USD goes to your connected Stripe account.

GET/v1/tester/payout-bankstester

НСПК bank catalog for RUB SBP withdrawals: [{code, name}, …].

GET/v1/tester/withdrawals?currency=tester

Your withdrawal history per currency, newest first (phone_masked only).

POST/v1/tester/stripe/connecttester

Create/reuse your Stripe Express account; returns the onboarding URL.

Ordering a test (task/v1)

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"}.

Reading the verdict (report/v1)

When a tester submits, their report appears in the task detail envelope's reports list. Reports are validated against report/v1:

  • recording_key — session video, required (anti-fraud gate).
  • bugs[] — severity (blocker/major/minor/trivial), title, steps, expected, actual, optional evidence keys.
  • ux_findings[] — area, 1–5 rating, optional comment.
  • metrics — completed, time_on_task_s, optional fun rating, onboarding clarity, perf issues.
# 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/approve

Task lifecycle

created → 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.

Errors

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.

MCP server — the native agent channel

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/schema
order_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"}.