← ThinkAloud · API keys (profile) · English · Dansk

ThinkAloud API

ThinkAloud is API-first: the UI uses exactly the same endpoints described here. Built for server-to-server integration (e.g. an analytics tool as an add-on: measurement finds the page with the high exit rate and the search query — ThinkAloud simulates *why* people leave). No CORS is opened on purpose: call from your backend, not from the browser.

Base URL: https://thinkaloud.activero.net (pilot; one machine — run one test at a time).

Auth — two ways:

  1. API key (recommended, Bureau plan): Authorization: Bearer ta_... — created at /app/profile → "API keys" (shown only once; up to 10 active, revoke individually). The key carries account context: runs are attributed to the account, credits are deducted, and GET /api/runs lists only the account's own runs.
  2. Access code (pilot / internal integration): X-Access-Code: <code>. The server's THINKALOUD_ACCESS_CODE is a comma-separated list — give each integration its own code so it can be revoked independently. No account context (no crediting/ownership).

Read endpoints require one of: a signed ?t= token (from status/list responses), ownership, or an access code. The 401 detail is always "access_denied".

Start a run

POST /api/runs
Content-Type: application/json
Authorization: Bearer ta_...

{
  "url": "https://example.com/page",
  "goal": "Understand what the page offers, and whether it is relevant to you.",
  "contentType": "dissemination",          // conversion | dissemination | reference
  "mode": "thinkaloud",                    // thinkaloud (UX) | e2e (functional test)
  "lang": "en",                            // output language for personas + report (en | da)
  "model": "glm",                          // comma-sep.: glm (default, cheapest),gemini,claude,llama,mistral (EU),grok (premium)
  "steps": 8,                              // steps per persona (e2e flows: use 15)
  "synthesisModel": "grok",                // optional premium pen: WRITES the recommendations/
                                           //   synthesis (personas keep their own model).
                                           //   Comprehensive runs use grok automatically.
  "personas": [
    {"id": "searcher", "label": "Searcher with concrete intent",
     "role": "You googled '<query from Search Console>' and landed here. You are impatient...",
     "style": "maalrettet"}                // optional: metodisk (reads everything, in order) |
  ]                                        //   rodet (skims, jumps around) | maalrettet (beelines)
}
→ {"run_id": "20260722-091653-f5fc0f"}

For search-query integrations: build the persona's role from the real query ("You googled X …") instead of an invented profile — that is the whole point.

Gated pages (login/staging)

Three optional fields in the same POST /api/runs body open pages behind a gate — TEST ACCOUNTS ONLY (the personas click around for real, including destructive buttons):

{
  "auth": {"user": "test", "password": "..."},        // HTTP Basic (staging gates)
  "headers": {"X-Api-Key": "..."},                     // arbitrary headers, e.g. a key-gated site
  "storage_state": "{\"cookies\": [...]}"              // session injection: log in yourself, export
}                                                      //   the session (Playwright storageState OR a
                                                       //   cookie-editor array) → SSO/2FA without
                                                       //   sharing a password

Security: secrets are handed to the browser harness via ephemeral env variables only — the server-side config.json keeps redacted placeholders (***), and they never appear in the report or the log. Form login with a test account also works without these fields: put the account in goal ("log in as test@… / password …") and the persona fills in the login form itself.

Poll status

GET /api/runs/{run_id}
→ {"run_id": ..., "status": "running|done|error", "log": "<tail>",
   "progress": {"phase": "reading", "pct": 42, "text": "Persona · Model", ...}}

Poll every 10-15 s. A run typically takes 1-5 min (more personas/models = longer; per-page first-glance anchoring costs ~25 s per new unique page the personas visit).

Fetch results

GET /api/runs/{run_id}/results          ← machine-readable (integrations)
GET /api/runs/{run_id}/report           ← HTML report (humans; sharing: the link is open)
GET /api/runs/{run_id}/report.pdf       ← PDF

/results (only when status: "done"):

{
  "run_id": "...", "status": "done",
  "url": "...", "contentType": "...", "goal": "...", "mode": "thinkaloud", "lang": "en",
  "firstGlancePriming": true,            // personas' first impression was saliency-anchored
  "modelsUsed": ["Gemini-2.5"],
  "personas": [{"label": "...", "model": "Gemini-2.5", "endedBy": "done"}],
  "outcomes":  [ /* e2e mode: {label, model, succeeded, endedBy, steps, blocker, url, step} */ ],
  "issues":    [{"title": "...", "field": "tryghed|mening|kan|besvaer|defekt",
                 "summary": "...", "nPersonas": 2, "avgSev": 3.5, "models": ["Gemini-2.5"],
                 "pts": [{"label": "...", "model": "...", "url": "...", "what": "...",
                          "severity": 4, "step": 3}]}],
  "positives": [ /* same shape, severity 0 */ ],
  "attention": {"model": "deepgaze-iie", "on_action_total": 0.21, "off_action": 0.79,
                "elements": [{"name": "...", "kind": "handling|overskrift|billede",
                              "share": 0.17, "primary": true}]}
}

The field values are the behavioural-friction fields (in Danish, stable as API identifiers): tryghed = trust, mening = meaning, kan = ability, besvaer = effort, defekt = defect.

Interpretation: issues are sorted worst-first (most personas → most models → highest average severity). nPersonas >= 2 = convergent. Everything is prediction, not measurement — label it that way in your UI. The validation loop: compare predicted friction with the actual exit rate.

Other endpoints

POST /api/panel              {brief|candidates, ...}  → focus group (synchronous, 1-3 min;
                                                       ~25 credits). model default =
                                                       "claude,glm": a variance test
                                                       (2026-08-24) showed single-model panels
                                                       can flip the winner between runs — two
                                                       models reproduced the same verdict 3/3
POST /api/suggest-personas   {url, goal?, lang?}     → suggest personas from the page (gated)
GET  /sample                                          → static example report (open)
GET  /api/defaults?lang=en                            → default personas + {"gated": true}
GET  /api/keys · POST /api/keys · DELETE /api/keys/{id}  → API-key management
                                                       (requires a login session, NOT a key/code)
GET  /api/auth/me                                     → account + credit balance (works w. Bearer)

MCP server

The thinkaloud-mcp npm package exposes all of the above as tools in Claude Desktop/Code and other MCP clients — thinkaloud_start_test, thinkaloud_test_status, thinkaloud_test_results, thinkaloud_list_tests, thinkaloud_focus_group, thinkaloud_suggest_personas, thinkaloud_account. One-line setup in Claude Code:

claude mcp add thinkaloud -e THINKALOUD_API_KEY=ta_YOUR_KEY -- npx -y thinkaloud-mcp

Auth via THINKALOUD_API_KEY (the Bearer key above). Source lives in mcp/ in the repo.