# Persona Lab API contract Base URL: `https://persona.spbros.com`. Use the paths below relative to this origin, including `/api`; do not append `/v1`. The portable helper manages private snapshots, state, and idempotency. Prefer it to writing another client. ## Authentication and defaults Every survey API request requires `Authorization: Bearer ` or `X-API-Key: `. Do not send conflicting headers. Keys belong only in headers, never URLs or stored reports. The model provider key stays on the server. Direct internal `/openapi.json` and `/docs` require authentication; the public origin exposes `/api/*` and this reference. Skill downloads are public. `GET /api/blog-review/defaults` is a read-only legacy-named settings endpoint used by both reaction and purchase surveys. It makes no model calls. Read `ready`, `reader_selection`, `reader_count`, `model_ids`, `sources` when present, `pool_revision`, `dataset_id`, `dataset_revision`, `exposed_fields`, and `available_readers`. Current configuration is 20 distinct random people and `gpt-6-luna`; use returned settings. This check does not verify worker liveness or provider availability. `sources` is a list of pinned pairs, for example `[{"dataset_id":"source-one","dataset_revision":"revision-one"},{"dataset_id":"source-two","dataset_revision":"revision-two"}]`. When present, use that entire list; do not select only the first source through the legacy `dataset_id`/`dataset_revision` fields. `pool_revision` identifies the combined selection pool, including its exclusion list, and is not a revision of either individual dataset. Source revisions stay unchanged. Save the original sources and pool revision with state; existing surveys preserve their frozen profiles. The server retains 1,100,000 originals (1,000,000 NVIDIA + 100,000 v2), while catalog queries and new selections exclude 3,656 clear v2 inconsistencies, leaving 1,096,344 eligible profiles (1,000,000 + 96,344). Review flags alone are not confirmed errors or automatic exclusions; 261 of the 11,182 review-flagged records overlap the clear inconsistencies and are excluded on that ground. Verify actual readiness/eligible counts through `available_readers`. The eligible v2 profiles cover ages 10–49 and include 10,168 people under 19. The five v2 SNS-related fields are synthetic estimates, not observed or independently validated account behavior. For a user-requested keyword, `GET /api/personas?sources=&q=...&limit=1` returns eligible matching `total` across the combined pool. Serialize the saved list with JSON, then URL-encode that value. Omit `dataset_id` and `dataset_revision` when supplying `sources`; combining them is rejected. Without `sources` in defaults, retain `GET /api/personas?dataset_id=...&dataset_revision=...&q=...&limit=1`. `q` is a literal, case-insensitive substring of saved background text or original ID. If fewer than `reader_count` match, report the shortfall before creating runs. ## Create and freeze a survey 1. `POST /api/panels/from-catalog` with JSON `name`, `count` from settings, a saved random integer `seed`, the saved `sources` list, `exposed_fields`, and `filters` (`{}` or `{"q": "user keyword"}`). Omit `dataset_id`/`dataset_revision` when using `sources`. If defaults have no `sources`, use the legacy `dataset_id`/`dataset_revision` pair instead. Save the returned `id`. A new survey selects new people; resume reuses the saved group and source list without reading new defaults. The defaults' `panel_id` describes the source settings, not the fresh selection. A frozen multi-source panel's `dataset_revision` may contain its pool revision; continue using saved `sources` rather than sending this as a single dataset revision. 2. `POST /api/experiments` creates a draft without model calls. Free reaction surveys use `survey_mode: "reaction"`, the user's question, and text or actual images: ```json { "name": "안내 자료 이해도 조사", "survey_mode": "reaction", "panel_id": "", "question": "이 내용을 어떻게 이해했나요? 헷갈리는 부분과 그 이유를 알려주세요.", "blog_post": "", "blog_images": [], "situation": {}, "variants": [{"id": "CONTENT", "label": "제공 자료", "products": []}], "repeats": 1 } ``` `blog_post` and `blog_images` retain legacy technical names; they carry general material. Reaction requires a nonblank question and text, images, or both. For images alone use `blog_post: null`. Ordered images are objects such as `{"name": "photo.png", "data_url": "data:image/png;base64,", "caption": "optional caption"}`. Only PNG/JPEG/WebP inline data URLs are accepted: 20 images, 5 MiB each, 30 MiB total decoded bytes. The server never fetches private URLs. IDs are `image-1`, `image-2`, etc. in attachment order. Explicit purchase intent uses `survey_mode: "purchase"`, a purchase question, nonblank `blog_post`, and one product per variant, for example `{"id":"POST","products":[{"id":"A","name":"자료에서 소개한 상품","total_price_krw":null}]}`. Unknown price stays `null`. Omitting `survey_mode` preserves the old purchase contract; the portable helper explicitly sends reaction for new default surveys. Existing product/price comparison payloads also remain purchase mode. 3. `POST /api/experiments/{id}/lock` with `{}` freezes material, question, mode, selected profiles, and images and returns `frozen`. Check against the submitted input before creating runs. Saving/freezing does not make model calls. ## Execute and read results `POST /api/experiments/{id}/runs?model_id=` with `{}` queues model work. Reaction uses `direct-http-v1`; EDSL does not support free reaction answers and is rejected. Use a stable `Idempotency-Key` per survey/model, between 1 and 200 characters. The helper retains the legacy prefix `blog-review::` to preserve resume compatibility. Save the returned run `id`. Reuse the same key after a lost response. - `GET /api/runs/{id}` returns `status` and `counts`. Terminal states are `completed`, `partial_failed`, and `cancelled`. - `GET /api/runs/{id}/report` includes `survey_mode`, the saved `question`, and `summary`. - `GET /api/runs/{id}/export?format=json` returns `report` and `cases`. Each case contains trace `id`, `persona_id`, `status`, `response`, and `case_input.exposed_profile`. Preserve `case_input.source_dataset`, `source_revision`, and `original_persona_id` when present and show the frozen source record with the actual exposed profile. Older exports may lack provenance fields. CSV and Markdown exports are also available. - Reaction responses contain a required nonblank `answer`, `reason`, `concerns`, `missing_information`, and `evidence_ids`. Example: `{"answer":"신청 기간과 방법을 안내하는 내용으로 이해했습니다.","reason":"기간과 신청 링크가 함께 표시되어 있습니다.","concerns":["마감 시간이 불명확합니다."],"missing_information":["마감 시각"],"evidence_ids":[]}`. No purchase choice is required. Summaries retain valid/invalid/error counts and per-person responses and profiles, without purchase fractions. - Purchase responses retain `choice`: `A` (purchase), `no_purchase`, or `insufficient_info`, with reasons and barriers. Product comparisons retain their existing choice contract. - `evidence_ids` may cite attached image IDs; citation does not prove accurate visual interpretation. Keep valid, failed, and unfinished counts separate. Resume the saved survey rather than creating another paid one after timeouts or connection failures. Helper exit codes: 0 completed, 2 running, 1 error/cancellation/partial failure. It does not silently retry failed model calls. Resume preserves the saved question/mode; conflicting options are rejected before requests. HTTP 401 means missing/incorrect access key; 429 means wait `Retry-After` seconds; 503 means server readiness/configuration trouble; 404 means missing ID/path; 409 means stored-state conflict; 422 means invalid input or unsupported setting. Per 60 seconds, defaults are 10 authentication failures per actual connection peer, 120 authenticated requests across the single key, and 5 model-run submissions or retries across that key. Public authentication failures share the web proxy peer's quota. Do not echo error bodies that may contain private material. Preserve the result directory and fix the cause before resuming; do not bypass throttling with new surveys or tight retry loops.