[ Trusted by builders from ]NetflixServiceNowCiscoAdobePayPalAmazonDatadogJPMorgan ChaseDell
[ Trusted by builders from ]NetflixServiceNowCiscoAdobePayPalAmazonDatadogJPMorgan ChaseDell
Prior.Runprior.run

FIG · 01— rest endpoint specification, v1

Ship after simulation.

The same engine you drive through the web UI, exposed as JSON over HTTPS. Build CI gates. Ship agents. Stop checking screenshots.

~/prior.run — curllive
$ 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" } … ] }
}

Production hostprod
https://api.prior.run
Local dev hostdev
http://localhost:8000
Bearer tokenrequired
Authorization: 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.

GET/api/v1/audienceslist saved audiences — pick your room01
GET/api/v1/persona/audience-panelopen the room — seats the members02
POST/api/v1/persona/audience-askask the whole room one question (fans out)03
POST/api/v1/persona/audience-upload-imageupload stimulus for an ask · image or short video04
POST/api/v1/persona/audience-synthesistranscript → themes + quotes + dissent05
POST/api/v1/persona/audience-member-dismissswap one seat for a fresh draw06
POST/api/v1/persona/audience-room/resetclear | swap | new · describe who to seat07
GET/api/v1/persona/audience-room/turnsserver-side transcript + field notes08
POST/api/v1/persona/audience-room/threadscreate a campaign thread (+ /rename)09
POST/api/v1/persona/audience-member-interview1:1 with a single member (+ GET /audience-member-history)10

The 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).

POST/api/v1/url/auditwalk one live URL · mode = explore | flow | quiz01
POST/api/v1/url/comparetwo URLs head-to-head02
GET/api/v1/url/audit/costcredit cost preview03
GET/api/v1/url/compare/costcredit cost preview04

Mine 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.

POST/api/v1/moodkick off pipeline (returns job_id)01
GET/api/v1/mood/jobs/{id}poll job status02
GET/api/v1/moodlist caller's reports03
GET/api/v1/mood/{id}full corpus JSON04
GET/api/v1/mood/{id}/cohortslist cohort_ids for spawn05
POST/api/v1/mood/{id}/regeneratefresh mine, same URL06
POST/api/v1/mood/{id}/spawn-audiencespanel → synthetic audiences07

GET /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.

GET/api/v1/memo/{id}archived memo · accepts memo UUID or share_token01
GET/api/v1/audience-templateslist built-in audience templates02
GET/api/v1/audienceslist caller's saved custom audiences (id + name)03
PATCH/api/v1/audiences/{id}rename a saved custom audience04
DELETE/api/v1/audiences/{id}delete a saved custom audience05

One-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.

POST/api/v1/persona/interviewask one persona on a focus group (capped per viewer)01
GET/api/v1/persona/interview/historytranscript + remaining budget02
POST/api/v1/persona/interview-upload-imageupload stimulus for a turn · images only03

01 · kickoff↳ blocks until the panel finishes
01curl -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 }'
02 · response↳ the walk itself · ~5–10 min
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}

rule i

Images


Base64, data URL, or https. Max 10 MB.

rule ii

Dedup


The same file uploaded twice is rejected before the room runs (400).

rule iii

Text limits


campaign_context: 2000. instructions: 4000. hypothesis: 500. creative_name: 60. metric: 120. Sanitized for injection.

rule iii·b

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.

rule iv

Live URLs


https only, max 2048 chars. url_a ≠ url_b for compare.

rule v

Mood


Async pipeline. POST returns job_id · poll /mood/jobs/{id} until status=completed.

rule vi

Auth


Bearer pr_live_… . Keys are user-scoped.

rule vii

Blocking


URL endpoints block until the panel finishes (~5–10 min) and return the walk. No polling.


Error codes↳ HTTP status · meaning
400Invalid input — bad JSON, missing fields, duplicate images, oversized text.
401Missing or invalid bearer token. Generate a new key at /settings.
403Quota exhausted or plan does not include this endpoint.
404walk_id, job_id, archived memo, or template not found for this account.
413Payload too large — image over 10 MB or URL over 2048 chars.
429Rate limit exceeded. Respect the Retry-After header before retrying.
500Server error. Safe to retry the same request once.
504The walk outran the gateway timeout. Do not re-submit — the run is already billed.
rule i

Rate limits


Per-key request rate and concurrent-job caps apply. Bursts → 429 with Retry-After. Upgrade plan to raise the ceiling.

rule ii

Polling


Mood is the only polled endpoint — /mood/jobs/{id} every ~5s until status=completed. URL walks block and return.

rule iii

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 ]

Or skip the curl.
Use an agent.