The same engine you drive through the web UI, exposed as JSON over HTTPS. Build CI gates. Ship agents. Stop checking screenshots.
$ curl https://api.prior.run/api/v1/url/compare -H "Authorization: Bearer $PR_KEY" -d '{"url_a": "…", "url_b": "…", "custom_audience_id": "…", "panel_member_indices": [0,1,2,3,4,5], …}' // running ··························· ← { "walk_id": "8f21c4e0…", "status": "complete", "walk": { head_to_head: { A: 4, B: 2 }, winner: "A", verdicts: [ { name: "Priya R.", pick: "A" } … ] } }▍
https://api.prior.runhttp://localhost:8000Authorization: Bearer pr_live_••••••••••••Generate at /settings. Plaintext shown once.
The front door — everything runs through a room, same as the web app, in two speeds. FAST is audience-ask: the whole room answers one question in seconds — conversation, structured actions, quick reads. DEEP is the URL endpoints below: the same seated members walk a live page in a real browser and report back what they did — journeys, reactions, hesitations, and a head-to-head vote on a compare (minutes, credits-charged). Open the panel for a saved audience, then ask the whole room one question and it fans out to each member in parallel. audience-ask runs structured actions — angle_test / hook_rank (options), script_react (script), ab_compare (2 images), creative_rank (3–5 images) — returning aggregated votes in result. A ranking of 3+ options collects two votes per member: the one they'd stop for and the one they'd scroll past, so result carries tally, scroll_tally and score (tally − scroll_tally), and score decides the winner. Members may decline to vote when the stimulus isn't theirs to judge: those answers carry abstained and cast no vote. A winner is only named when at least half the room voted and the leader clears the runner-up by more than one point — otherwise result reports no_read, close_call or tie, and winner_index is null. Options with a negative score are the kill list. Stimulus comes from audience-upload-image — png / jpeg / webp / gif images (≤ 8 MB) or short mp4 / mov / webm ad videos (≤ 100 MB): the response's kind routes the URL into image_urls or video_urls (up to 5 videos per turn; clips are prescanned at upload so the panel reacts to what's actually on screen, and 2 videos = A/B vote, 3-5 = ranked vote — same counterbalanced tally as images, and a 2-video A/B renders the same verdict card). target_indices @-targets specific members (untagged members may interject once). Manage the room like the web app: dismiss a member for a fresh draw, reset (clear / swap / new), scope turns to campaign threads via thread_id, and rebuild history server-side from audience-room/turns. A "new" reset also takes an optional description of who you want in the room — the members of that audience who best match are seated instead of a random draw, each with the detail that earned the seat, and panel_match.matched reports how many genuinely fit. Members are only ever selected from the audience you built; nobody is generated to match the description, so an audience that lacks those people says so rather than inventing them. A following "swap" walks the same ranking to the next-best seats. Capped per audience to keep the panel honest. A URL question whose page can't be fetched (JS-only apps) returns 422 {code: "page_unreadable"} without spending a message — re-send with allow_unreadable_page: true to have the panel react to the link from expectation instead. Comparing something against itself is caught before the room runs: passing the same file twice returns 400 naming the pair, while the same ad re-exported at another encoding is allowed but reframed — result is null, options empty, no per-member pick, and a duplicate_note explains that the room reacted to one ad instead of picking a winner (an absent result there is not a tie). audience-synthesis may also return an advisory handoff object — {discipline, room_label, reason, and target_audience_id + target_name when it points back at an earlier room} — flagging a specialist read (legal, visual design, …) worth getting next; act on it by creating an audience for that discipline and re-running the question there. When you're ready to go deep, pass the panel's audience id + member indices to the URL endpoints below.
/api/v1/audiences↳ list saved audiences — pick your room01/api/v1/persona/audience-panel↳ open the room — seats the members02/api/v1/persona/audience-ask↳ ask the whole room one question (fans out)03/api/v1/persona/audience-upload-image↳ upload stimulus for an ask · image or short video04/api/v1/persona/audience-synthesis↳ transcript → themes + quotes + dissent05/api/v1/persona/audience-member-dismiss↳ swap one seat for a fresh draw06/api/v1/persona/audience-room/reset↳ clear | swap | new · describe who to seat07/api/v1/persona/audience-room/turns↳ server-side transcript + field notes08/api/v1/persona/audience-room/threads↳ create a campaign thread (+ /rename)09/api/v1/persona/audience-member-interview↳ 1:1 with a single member (+ GET /audience-member-history)10The room's deep page walk — pass custom_audience_id + panel_member_indices (both required, from your open panel) and your seated members walk the real website in a live browser. You get back what each of them did: the journey, what they said, where they hesitated, and whether they finished. A compare adds head_to_head — one vote per panelist, from the people who walked both pages — and winner (null when the room splits evenly). Pass mode=explore for free roam, flow for a guided funnel, or quiz for a verdict-pressure walk. Pass instructions to brief the panel — what this is, what to skip, and anything you want answered; they read it before starting and each panelist answers your asks at the end. Pass lens to say what their comments should be about — content for the words and what they claim, ux for the design and how it behaves, or omit for both; it narrows the subject of the feedback, not how much of it you get. Design prototypes (Figma, InVision, Marvel, Maze and others) are detected from the URL and judged as unbuilt work rather than a live product — override with is_prototype for a tool we don't recognise. Credits-charged per run. Long-running — the endpoint blocks until every panelist finishes walking (~5–10 min).
/api/v1/url/audit↳ walk one live URL · mode = explore | flow | quiz01/api/v1/url/compare↳ two URLs head-to-head02/api/v1/url/audit/cost↳ credit cost preview03/api/v1/url/compare/cost↳ credit cost preview04Mine the open web for what real people say about a brand. Asynchronous — POST returns a job_id immediately, poll until status='completed' (~15–25 min), then GET the corpus.
/api/v1/mood↳ kick off pipeline (returns job_id)01/api/v1/mood/jobs/{id}↳ poll job status02/api/v1/mood↳ list caller's reports03/api/v1/mood/{id}↳ full corpus JSON04/api/v1/mood/{id}/cohorts↳ list cohort_ids for spawn05/api/v1/mood/{id}/regenerate↳ fresh mine, same URL06/api/v1/mood/{id}/spawn-audiences↳ panel → synthetic audiences07GET /memo/{id} reads the archive. Memos were retired on 30 Jul 2026 — nothing produces new ones, and the URL endpoints return the walk itself rather than a written-up document. This endpoint stays so memos created before that date, and the links shared from them, keep resolving. Accepts either the internal UUID or the share_token.
/api/v1/memo/{id}↳ archived memo · accepts memo UUID or share_token01/api/v1/audience-templates↳ list built-in audience templates02/api/v1/audiences↳ list caller's saved custom audiences (id + name)03/api/v1/audiences/{id}↳ rename a saved custom audience04/api/v1/audiences/{id}↳ delete a saved custom audience05One-shot follow-up Q&A with a single synthetic persona on a focus group. Show them images this turn (image_urls, from interview-upload-image — images only) for a gut reaction. Capped per (focus group, persona, viewer) to keep replies honest.
/api/v1/persona/interview↳ ask one persona on a focus group (capped per viewer)01/api/v1/persona/interview/history↳ transcript + remaining budget02/api/v1/persona/interview-upload-image↳ upload stimulus for a turn · images only0301curl -X POST https://api.prior.run/api/v1/url/compare \02 -H "Authorization: Bearer $PRIORRUN_API_KEY" \03 -H "Content-Type: application/json" \04 -d '{05 "url_a": "https://brand.com/landing-a",06 "url_b": "https://brand.com/landing-b",07 "instructions": "Gen Z skincare launch — where would they bounce?",08 "lens": ["ux"],09 "custom_audience_id": "aud_9f2c…",10 "panel_member_indices": [0, 1, 2, 3, 4, 5]11 }'
01{02 "walk_id": "8f21c4e0…",03 "status": "complete",04 "walk": {05 "head_to_head": { "A": 4, "B": 2 },06 "winner": "A",07 "verdicts": [ { "name": "Priya R.", "pick": "A",08 "verdict": "B buried the price…" } ],09 "audience": { "A": { "completion_rate": 0.67, … } }10 }11}
Images
Base64, data URL, or https. Max 10 MB.
Dedup
The same file uploaded twice is rejected before the room runs (400).
Text limits
campaign_context: 2000. instructions: 4000. hypothesis: 500. creative_name: 60. metric: 120. Sanitized for injection.
Room-scoped
Every deep-run endpoint requires custom_audience_id + panel_member_indices — open the room with GET /persona/audience-panel first and pass its audience id + seated member indices, same as the web app.
Live URLs
https only, max 2048 chars. url_a ≠ url_b for compare.
Mood
Async pipeline. POST returns job_id · poll /mood/jobs/{id} until status=completed.
Auth
Bearer pr_live_… . Keys are user-scoped.
Blocking
URL endpoints block until the panel finishes (~5–10 min) and return the walk. No polling.
Rate limits
Per-key request rate and concurrent-job caps apply. Bursts → 429 with Retry-After. Upgrade plan to raise the ceiling.
Polling
Mood is the only polled endpoint — /mood/jobs/{id} every ~5s until status=completed. URL walks block and return.
Key lifecycle
Keys are user-scoped, shown in plaintext once at creation. Rotate or revoke any time at /settings. Old keys 401 instantly.
↳ Duplicate image submissions inside the same compare body are rejected with 400 — treat this as built-in idempotency for re-submits within a short window.
[ end of specification ]